diff --git a/src/app/@ansyn/ansyn/app-effects/effects/cases/select-case.app.effects.spec.ts b/src/app/@ansyn/ansyn/app-effects/effects/cases/select-case.app.effects.spec.ts index db774fe25e..d13b7ad3f0 100644 --- a/src/app/@ansyn/ansyn/app-effects/effects/cases/select-case.app.effects.spec.ts +++ b/src/app/@ansyn/ansyn/app-effects/effects/cases/select-case.app.effects.spec.ts @@ -78,7 +78,7 @@ describe('SelectCaseAppEffects', () => { const orientation: CaseOrientation = 'Imagery Perspective', geoFilter: CaseGeoFilter = CaseGeoFilter.PinPoint, - timeFilter: CaseTimeFilter = 'Start - End', + timeFilter: CaseTimeFilter = CaseTimeFilter.StartEnd, time: CaseTimeState = { type: 'absolute', from: new Date(0), to: new Date(0) }, region: CaseRegionState = {}, dataInputFilters: CaseDataInputFiltersState = { fullyChecked: true, filters: [], active: true }, diff --git a/src/app/@ansyn/ansyn/app-effects/effects/filters.app.effects.ts b/src/app/@ansyn/ansyn/app-effects/effects/filters.app.effects.ts index 5cce42f0e1..581ce3c23d 100644 --- a/src/app/@ansyn/ansyn/app-effects/effects/filters.app.effects.ts +++ b/src/app/@ansyn/ansyn/app-effects/effects/filters.app.effects.ts @@ -35,9 +35,9 @@ import { import 'rxjs/add/operator/share'; import 'rxjs/add/observable/of'; import { SetBadgeAction } from '@ansyn/menu/actions/menu.actions'; -import { selectFavoriteOverlays } from '@ansyn/core/reducers/core.reducer'; +import { selectFavoriteOverlays, selectOverlaysCriteria } from '@ansyn/core/reducers/core.reducer'; import { CaseFacetsState, CaseFilter, FilterType } from '@ansyn/core/models/case.model'; -import { Overlay } from '@ansyn/core/models/overlay.model'; +import { Overlay, OverlaysCriteria } from '@ansyn/core/models/overlay.model'; import { FilterMetadata } from '@ansyn/menu-items/filters/models/metadata/filter-metadata.interface'; import { FiltersService } from '@ansyn/menu-items/filters/services/filters.service'; import { FilterModel } from '@ansyn/core/models/filter.model'; @@ -69,10 +69,10 @@ export class FiltersAppEffects { */ @Effect() updateOverlayFilters$ = this.onFiltersChanges$ - .withLatestFrom(this.overlaysArray$) - .mergeMap(([[filters, showOnlyFavorite, favoriteOverlays], overlaysArray]: [[Filters, boolean, Overlay[]], Overlay[]]) => { + .withLatestFrom(this.overlaysArray$, this.store$.select(selectOverlaysCriteria)) + .mergeMap(([[filters, showOnlyFavorite, favoriteOverlays], overlaysArray, { time }]: [[Filters, boolean, Overlay[]], Overlay[], OverlaysCriteria]) => { const filterModels: FilterModel[] = FiltersService.pluckFilterModels(filters); - const filteredOverlays: string[] = OverlaysService.buildFilteredOverlays(overlaysArray, filterModels, favoriteOverlays, showOnlyFavorite); + const filteredOverlays: string[] = OverlaysService.buildFilteredOverlays(overlaysArray, filterModels, favoriteOverlays, showOnlyFavorite, time); const message = (filteredOverlays && filteredOverlays.length) ? overlaysStatusMessages.nullify : overlaysStatusMessages.noOverLayMatchFilters; return [ new SetFilteredOverlaysAction(filteredOverlays), diff --git a/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/multiple-source-provider.spec.ts b/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/multiple-source-provider.spec.ts index 122319b180..b25a677eef 100644 --- a/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/multiple-source-provider.spec.ts +++ b/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/multiple-source-provider.spec.ts @@ -110,7 +110,7 @@ const regionCoordinates = [ const fetchParams: any = { limit: 250, region: turf.geometry('Polygon', regionCoordinates), - timeRange: {start: new Date().toISOString(), end: new Date().toISOString()} + timeRange: [{start: new Date().toISOString(), end: new Date().toISOString()}] }; const fetchParamsWithLimitZero: any = { ...fetchParams, limit: 0}; diff --git a/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/multiple-source-provider.ts b/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/multiple-source-provider.ts index fd0816559b..414aea752d 100644 --- a/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/multiple-source-provider.ts +++ b/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/multiple-source-provider.ts @@ -1,7 +1,7 @@ import { Observable } from 'rxjs/Observable'; import { Inject, Injectable, InjectionToken } from '@angular/core'; import { - BaseOverlaySourceProvider, DateRange, IFetchParams, OverlayFilter, + BaseOverlaySourceProvider, IFetchParams, OverlayFilter, StartAndEndDate } from '@ansyn/overlays/models/base-overlay-source-provider.model'; import { Overlay, OverlaysFetchData } from '@ansyn/core/models/overlay.model'; @@ -9,10 +9,11 @@ import { Feature, Polygon } from 'geojson'; import { LoggerService } from '@ansyn/core/services/logger.service'; import { area, intersect, difference } from '@turf/turf'; import { DataInputFilterValue } from '@ansyn/core/models/case.model'; +import { IDateRange } from '@ansyn/core/models/time.model'; export interface FiltersList { name: string, - dates: DateRange[] + dates: IDateRange[] sensorNames: string[], coverage: number[][][][] } @@ -142,7 +143,7 @@ export class MultipleOverlaysSourceProvider extends BaseOverlaySourceProvider { errors, data: null, limited: -1 - } + }; } return this.mergeOverlaysFetchData(overlays, fetchParams.limit, errors); diff --git a/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/open-aerial-source-provider.ts b/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/open-aerial-source-provider.ts index 640527e0ea..44998a43c3 100644 --- a/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/open-aerial-source-provider.ts +++ b/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/open-aerial-source-provider.ts @@ -57,8 +57,8 @@ export class OpenAerialSourceProvider extends BaseOverlaySourceProvider { platform: 'uav', limit: `${fetchParams.limit + 1}`, bbox: `${bbox[0]},${bbox[1]},${bbox[2]},${bbox[3]}`, - acquisition_from: fetchParams.timeRange.start.toISOString(), - acquisition_to: fetchParams.timeRange.end.toISOString() + acquisition_from: fetchParams.timeRange[0].start.toISOString(), + acquisition_to: fetchParams.timeRange[0].end.toISOString() }; return this.http.get(baseUrl, { params: params }) diff --git a/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/planet/planet-source-provider.ts b/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/planet/planet-source-provider.ts index c26d09e1ff..6d1c4ef598 100644 --- a/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/planet/planet-source-provider.ts +++ b/src/app/@ansyn/ansyn/app-providers/overlay-source-providers/planet/planet-source-provider.ts @@ -61,11 +61,11 @@ export class PlanetSourceProvider extends BaseOverlaySourceProvider { }); } - buildFilters(config: IPlanetFilter[], sensors?: string[]) { + buildFilters(config: IPlanetFilter[], sensors?: string[], filterType = 'AndFilter') { return { item_types: Array.isArray(sensors) ? sensors : this.planetOverlaysSourceConfig.itemTypes, filter: { - type: 'AndFilter', + type: filterType, config: config } }; @@ -86,10 +86,7 @@ export class PlanetSourceProvider extends BaseOverlaySourceProvider { const limit = `${fetchParams.limit + 1}`; const bboxFilter = { type: 'GeometryFilter', field_name: 'geometry', config: fetchParams.region }; - const dateFilter = { - type: 'DateRangeFilter', field_name: 'acquired', - config: { gte: fetchParams.timeRange.start.toISOString(), lte: fetchParams.timeRange.end.toISOString() } - }; + const dateFilter = this.buildDateFilter(fetchParams); const filters: IPlanetFilter[] = [bboxFilter, dateFilter]; @@ -129,6 +126,39 @@ export class PlanetSourceProvider extends BaseOverlaySourceProvider { }); } + // build date filter for single / multiple time ranges + buildDateFilter(fetchParams: IFetchParams) { + let dateFilter; + if (fetchParams.timeRange.length === 1) { + // single time range (like in start-end) + dateFilter = { + type: 'DateRangeFilter', + field_name: 'acquired', + config: { + gte: fetchParams.timeRange[0].start.toISOString(), + lte: fetchParams.timeRange[0].end.toISOString() + } + } + } else { + // multiple time ranges (like in intervals) + const dateRangeFilters = fetchParams.timeRange.map(tr => { + return { + type: 'DateRangeFilter', + field_name: 'acquired', + config: { + gte: tr.start.toISOString(), + lte: tr.end.toISOString() + } + } + }); + dateFilter = { + type: 'OrFilter', + config: dateRangeFilters + }; + } + return dateFilter; + } + getById(id: string, sourceType: string): Observable { const baseUrl = this.planetOverlaysSourceConfig.baseUrl; const body = this.buildFilters([{ type: 'StringInFilter', field_name: 'id', config: [id] }]); diff --git a/src/app/@ansyn/assets/styles/styles.less b/src/app/@ansyn/assets/styles/styles.less index 80ca2b972e..67060b9426 100644 --- a/src/app/@ansyn/assets/styles/styles.less +++ b/src/app/@ansyn/assets/styles/styles.less @@ -180,12 +180,6 @@ button[disabled] { } } -input[type=number]::-webkit-inner-spin-button, -input[type=number]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} - @font-face { font-family: 'AnSyn'; src: url('/assets/fonts/AnSyn/AnSyn.eot?mxpept&v=1'); diff --git a/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.html b/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.html new file mode 100644 index 0000000000..40ea69d8a2 --- /dev/null +++ b/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.html @@ -0,0 +1,2 @@ + + diff --git a/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.less b/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.less new file mode 100644 index 0000000000..15bc652d23 --- /dev/null +++ b/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.less @@ -0,0 +1,15 @@ +@import "../../less/common"; + +:host { + position: relative; + input.flatpickr-input { + width: 130px; + height: 20px; + padding: 0 5px; + } + i { + position: absolute; + right: 10px; + top: 4px; + } +} diff --git a/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.spec.ts b/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.spec.ts new file mode 100644 index 0000000000..27203c5393 --- /dev/null +++ b/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DatePickerComponent } from './ansyn-date-picker.component'; + + +describe('DatePickerComponent', () => { + let component: DatePickerComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [DatePickerComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DatePickerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); + diff --git a/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.ts b/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.ts new file mode 100644 index 0000000000..c46fd854c4 --- /dev/null +++ b/src/app/@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component.ts @@ -0,0 +1,99 @@ +import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; +import * as flatpickr from 'flatpickr'; + +@Component({ + selector: 'ansyn-date-picker', + templateUrl: './ansyn-date-picker.component.html', + styleUrls: ['./ansyn-date-picker.component.less'] +}) + +export class DatePickerComponent implements OnInit { + + private _datePickerValue: Date; + private _datePickerInstance: any; + + @ViewChild('datePicker') datePicker: ElementRef; + @Output() dateChange = new EventEmitter(); + + @Input() disabled = false; + + @Input() + set date(val) { + this._datePickerValue = val; + if (this._datePickerInstance) { + this._datePickerInstance.setDate(this._datePickerValue, false); + } + } + + constructor() {} + + ngOnInit() { + // see API: https://flatpickr.js.org/options/ + this._datePickerInstance = new (flatpickr)(this.datePicker.nativeElement, { + // id: 'end', + time_24hr: true, + enableTime: true, + dateFormat: 'H:i d/m/Y', + defaultDate: this._datePickerValue ? this._datePickerValue : new Date(), + onChange: this.onDateChange.bind(this), + plugins: [this.confirmDatePlugin({})] + }); + } + + confirmDatePlugin(pluginConfig: any) { + const defaultConfig = { + confirmIcon: ' ', + confirmText: 'OK ', + showAlways: false, + theme: 'light' + }; + + const config = Object.assign(defaultConfig, pluginConfig); + + return function (fp) { + const hooks = { + onKeyDown: function onKeyDown(_, __, ___, e) { + if (fp.config.enableTime && e.key === 'Tab' && e.target === fp.amPM) { + e.preventDefault(); + fp.confirmContainer.focus(); + } else if (e.key === 'Enter' && e.target === fp.confirmContainer) { + fp.close(); + } + }, + onReady: function onReady() { + if (fp.calendarContainer === undefined) { + return; + } + + fp.confirmContainer = fp._createElement('div', 'flatpickr-confirm ' + (config.showAlways ? 'visible' : '') + ' ' + config.theme + 'Theme', config.confirmText); + + fp.confirmContainer.tabIndex = -1; + fp.confirmContainer.innerHTML += config.confirmIcon; + + fp.confirmContainer.addEventListener('click', fp.close); + fp.calendarContainer.appendChild(fp.confirmContainer); + } + } as any; + + if (!config.showAlways) { + hooks.onChange = function (dateObj, dateStr) { + const showCondition = fp.config.enableTime || fp.config.mode === 'multiple'; + if (dateStr && !fp.config.inline && showCondition) { + return fp.confirmContainer.classList.add('visible'); + } + fp.confirmContainer.classList.remove('visible'); + }; + } + + return hooks; + }; + } + + onDateChange(event) { + if (this.disabled) { + return false; + } + this._datePickerValue = event[0]; + this.dateChange.emit(this._datePickerValue); + } +} diff --git a/src/app/@ansyn/core/core.module.ts b/src/app/@ansyn/core/core.module.ts index 224ba50bcd..8351a55319 100644 --- a/src/app/@ansyn/core/core.module.ts +++ b/src/app/@ansyn/core/core.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AnsynCheckboxComponent } from './components/ansyn-checkbox/ansyn-checkbox.component'; +import { DatePickerComponent } from './components/ansyn-date-picker/ansyn-date-picker.component'; import { ImageryStatusComponent } from './components/imagery-status/imagery-status.component'; import { PlaceholderComponent } from './components/placeholder/placeholder.component'; import { StoreModule } from '@ngrx/store'; @@ -18,6 +19,7 @@ import { StorageService } from '@ansyn/core/services/storage/storage.service'; const coreComponents = [ AnsynCheckboxComponent, + DatePickerComponent, ImageryStatusComponent, PlaceholderComponent, ToastComponent, diff --git a/src/app/@ansyn/core/less/common.less b/src/app/@ansyn/core/less/common.less index af038df0b9..ad627ecaa2 100644 --- a/src/app/@ansyn/core/less/common.less +++ b/src/app/@ansyn/core/less/common.less @@ -11,6 +11,7 @@ @overlays-dots-color-hover: #bd0fe2; @menu-width: 90px; @menu-color: #2E2D46; +@text-disabled-color: #9D9D9D; @timeline-area-height: 110px; .default-menu-item { diff --git a/src/app/@ansyn/core/models/case.model.ts b/src/app/@ansyn/core/models/case.model.ts index 805d92e33e..ad9fe2da07 100644 --- a/src/app/@ansyn/core/models/case.model.ts +++ b/src/app/@ansyn/core/models/case.model.ts @@ -29,8 +29,11 @@ export interface IContextEntity extends IVisualizerEntity { } export type CaseOrientation = 'Align North' | 'User Perspective' | 'Imagery Perspective'; -export type CaseTimeFilter = 'Start - End'; +export enum CaseTimeFilter { + 'StartEnd' = 'Start - End', + 'Intervals' = 'Intervals' +} export enum CaseGeoFilter { PinPoint = 'Point', @@ -83,10 +86,24 @@ export interface CaseDataInputFiltersState { active: boolean } +export interface TimeIntervals { + interval: number // in milliseconds + criteria: CaseIntervalCriteria +} + export interface CaseTimeState { type: 'absolute', from: Date, to: Date + intervals?: TimeIntervals; +} + +export type CaseIntervalCriteriaType = 'best' | 'closest-before' | 'closest-after' | 'closest-both'; + +export interface CaseIntervalCriteria { + type: CaseIntervalCriteriaType, + before?: number, + after?: number, } export interface CaseBooleanFilterMetadata { diff --git a/src/app/@ansyn/core/models/intervals.model.ts b/src/app/@ansyn/core/models/intervals.model.ts new file mode 100644 index 0000000000..8637f92ed5 --- /dev/null +++ b/src/app/@ansyn/core/models/intervals.model.ts @@ -0,0 +1,22 @@ +import { Overlay } from '@ansyn/core/models/overlay.model'; + +export interface IntervalTimeFrame { + startDate: Date; + endDate: Date; + span: number; + intervalsCount: number; +} + +export interface IntervalTimeFrame { + startDate: Date; + endDate: Date; + span: number; + intervalsCount: number; +} + +export interface Interval { + startTime: Date; + endTime: Date; + pivot: Date; + overlays: Array; +} diff --git a/src/app/@ansyn/core/models/time.model.ts b/src/app/@ansyn/core/models/time.model.ts new file mode 100644 index 0000000000..a340079f53 --- /dev/null +++ b/src/app/@ansyn/core/models/time.model.ts @@ -0,0 +1,4 @@ +export interface IDateRange { + start: Date, + end: Date +} diff --git a/src/app/@ansyn/core/utils/sorting.ts b/src/app/@ansyn/core/utils/sorting.ts index 457fae3eb1..3ba77434ad 100644 --- a/src/app/@ansyn/core/utils/sorting.ts +++ b/src/app/@ansyn/core/utils/sorting.ts @@ -1,3 +1,4 @@ // latest date first export const sortByDate = (o1, o2) => new Date(o2.date).getTime() - new Date(o1.date).getTime(); export const sortByDateDesc = (o1, o2) => -sortByDate(o1, o2); +export const sortByResolutionDesc = (o1, o2) => o1.bestResolution > o2.bestResolution ? -1 : 1; diff --git a/src/app/@ansyn/menu-items/cases/reducers/cases.reducer.spec.ts b/src/app/@ansyn/menu-items/cases/reducers/cases.reducer.spec.ts index aa94fdedd5..e1ce3aa45c 100644 --- a/src/app/@ansyn/menu-items/cases/reducers/cases.reducer.spec.ts +++ b/src/app/@ansyn/menu-items/cases/reducers/cases.reducer.spec.ts @@ -7,8 +7,7 @@ import { } from '../actions/cases.actions'; import { Case } from '../models/case.model'; import { casesAdapter, CasesReducer, ICasesState, initialCasesState } from './cases.reducer'; -import { CaseGeoFilter } from '@ansyn/core/models/case.model'; -import { CasesService } from '@ansyn/menu-items/cases/services/cases.service'; +import { CaseGeoFilter, CaseTimeFilter } from '@ansyn/core/models/case.model'; describe('CasesReducer', () => { @@ -26,7 +25,7 @@ describe('CasesReducer', () => { }, orientation: 'Align North', dataInputFilters: { fullyChecked: true, filters: [], active: true }, - timeFilter: 'Start - End', + timeFilter: CaseTimeFilter.StartEnd, geoFilter: CaseGeoFilter.PinPoint, region: {}, overlaysManualProcessArgs: {} diff --git a/src/app/@ansyn/menu-items/filters/services/filters.service.ts b/src/app/@ansyn/menu-items/filters/services/filters.service.ts index b960f4a67d..f5b48f798f 100644 --- a/src/app/@ansyn/menu-items/filters/services/filters.service.ts +++ b/src/app/@ansyn/menu-items/filters/services/filters.service.ts @@ -67,7 +67,7 @@ export class FiltersService { const cloneFilters = new Map(filterState.filters); cloneFilters.set(metadataKey, cloneMetadata); const filterModels: FilterModel[] = FiltersService.pluckFilterModels(cloneFilters); - const filteredOverlays: string[] = OverlaysService.buildFilteredOverlays(Array.from(overlays.values()), filterModels, favoriteOverlays, filterState.facets.showOnlyFavorites); + const filteredOverlays: string[] = OverlaysService.buildFilteredOverlays(Array.from(overlays.values()), filterModels, favoriteOverlays, filterState.facets.showOnlyFavorites, { time: null }); metadata.resetFilteredCount(); filteredOverlays.forEach((id: string) => { const overlay = overlays.get(id); diff --git a/src/app/@ansyn/menu-items/tools/go-to/projections/geo/geo.component.less b/src/app/@ansyn/menu-items/tools/go-to/projections/geo/geo.component.less index f3107bdfde..c44f32578e 100644 --- a/src/app/@ansyn/menu-items/tools/go-to/projections/geo/geo.component.less +++ b/src/app/@ansyn/menu-items/tools/go-to/projections/geo/geo.component.less @@ -1,4 +1,11 @@ :host { + // remove html arrow controls from input[type=number] + input[type=number]::-webkit-inner-spin-button, + input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + box-sizing: border-box; display: flex; flex-direction: column; diff --git a/src/app/@ansyn/menu-items/tools/go-to/projections/utm/utm.component.less b/src/app/@ansyn/menu-items/tools/go-to/projections/utm/utm.component.less index 6b4eca4d54..c7eedb273d 100644 --- a/src/app/@ansyn/menu-items/tools/go-to/projections/utm/utm.component.less +++ b/src/app/@ansyn/menu-items/tools/go-to/projections/utm/utm.component.less @@ -1,4 +1,11 @@ :host { + // remove html arrow controls from input[type=number] + input[type=number]::-webkit-inner-spin-button, + input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + box-sizing: border-box; display: flex; flex-direction: column; diff --git a/src/app/@ansyn/overlays/models/base-overlay-source-provider.model.ts b/src/app/@ansyn/overlays/models/base-overlay-source-provider.model.ts index 2cd308cb05..8d2f4ee0ba 100644 --- a/src/app/@ansyn/overlays/models/base-overlay-source-provider.model.ts +++ b/src/app/@ansyn/overlays/models/base-overlay-source-provider.model.ts @@ -12,19 +12,21 @@ export interface DateRange { start: Date; end: Date; } +import { cloneDeep } from 'lodash'; +import { IDateRange } from '@ansyn/core/models/time.model'; export interface IFetchParams { limit: number; region: GeoJsonObject; sensors?: string[]; dataInputFilters: DataInputFilterValue[]; - timeRange: DateRange; + timeRange: Array; } export interface OverlayFilter { sensor: string; coverage: Feature; - timeRange: DateRange + timeRange: IDateRange } export interface StartAndEndDate { @@ -32,7 +34,7 @@ export interface StartAndEndDate { endDate: string } -export function timeIntersection(whiteRange: DateRange, blackRange: DateRange): DateRange { +export function timeIntersection(whiteRange: IDateRange, blackRange: IDateRange): IDateRange { if (!blackRange.end || (whiteRange.end && whiteRange.end <= blackRange.end)) { if (!blackRange.start || whiteRange.end > blackRange.start) { if (whiteRange.start >= blackRange.start) { @@ -64,11 +66,13 @@ export abstract class BaseOverlaySourceProvider { properties: {}, geometry: fetchParams.region }; - - // They are strings! - const fetchParamsTimeRange = { - start: new Date(fetchParams.timeRange.start), - end: new Date(fetchParams.timeRange.end) + // get time range data + const fetchParamsTimeRange = cloneDeep(fetchParams.timeRange); + const fetchParamsGlobalTimeRange = { + // min start value + start: new Date(fetchParams.timeRange.reduce((a, b) => a.start <= b.start ? a : b).start), + // max end value + end: new Date(fetchParams.timeRange.reduce((a, b) => a.end >= b.end ? a : b).end) }; const fetchObservables = filters @@ -77,13 +81,13 @@ export abstract class BaseOverlaySourceProvider { return intersection && intersection.geometry; }) // Make sure they have a common time range - .filter(f => Boolean(timeIntersection(fetchParamsTimeRange, f.timeRange))) + .filter(f => Boolean(timeIntersection(fetchParamsGlobalTimeRange, f.timeRange))) .map(f => { // Create new filters, by the common region and time let newFetchParams: IFetchParams = { ...fetchParams, region: intersect(f.coverage, regionFeature).geometry, - timeRange: timeIntersection(fetchParamsTimeRange, f.timeRange), + timeRange: fetchParamsTimeRange.map(tr => timeIntersection(tr, f.timeRange)) }; // Add sensor if exists on the filter diff --git a/src/app/@ansyn/overlays/services/overlays.service.ts b/src/app/@ansyn/overlays/services/overlays.service.ts index 8328585136..3ae6dcfb2c 100644 --- a/src/app/@ansyn/overlays/services/overlays.service.ts +++ b/src/app/@ansyn/overlays/services/overlays.service.ts @@ -1,5 +1,5 @@ import { BaseOverlaySourceProvider, StartAndEndDate } from '../models/base-overlay-source-provider.model'; -import { Inject, Injectable, InjectionToken } from '@angular/core'; +import { Inject, Injectable, InjectionToken, isDevMode } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Overlay } from '../models/overlay.model'; import { IOverlaysState, OverlayDrop, TimelineRange } from '../reducers/overlays.reducer'; @@ -9,23 +9,179 @@ import 'rxjs/add/operator/map'; import 'rxjs/add/operator/catch'; import { union } from 'lodash'; import { FilterModel } from '@ansyn/core/models/filter.model'; -import { sortByDateDesc } from '@ansyn/core/utils/sorting'; +import { sortByDate, sortByDateDesc, sortByResolutionDesc } from '@ansyn/core/utils/sorting'; +import { CaseIntervalCriteria, CaseTimeState } from '@ansyn/core/models/case.model'; +import { Interval, IntervalTimeFrame } from '@ansyn/core/models/intervals.model'; +import { times as doNTimes } from 'lodash'; +import { IDateRange } from '@ansyn/core/models/time.model'; export const OverlaysConfig: InjectionToken = new InjectionToken('overlays-config'); @Injectable() export class OverlaysService { - - static buildFilteredOverlays(overlays: Overlay[], parsedFilters: FilterModel[], favorites: Overlay[], showOnlyFavorite: boolean): string[] { + static buildFilteredOverlays(overlays: Overlay[], parsedFilters: FilterModel[], favorites: Overlay[], showOnlyFavorite: boolean, timeState?: CaseTimeState): string[] { let parsedOverlays: Overlay[] = favorites; if (!showOnlyFavorite) { const filteredOverlays = overlays.filter((overlay) => parsedFilters.every(filter => filter.filterFunc(overlay, filter.key))); parsedOverlays = [...parsedOverlays, ...filteredOverlays]; } + + if (timeState && timeState.intervals) { + // interval mode - limit overlays to be 1 per interval scope + parsedOverlays = OverlaysService.intervalFilter(parsedOverlays, timeState); + } + parsedOverlays.sort(sortByDateDesc); return union(parsedOverlays.map(({ id }) => id)); } + /* start */ + // limit overlays to be 1 per interval scope + static intervalFilter(overlays: Overlay[], timeState: CaseTimeState): Overlay[] { + const tf: IntervalTimeFrame = OverlaysService.calculateTimeFrame(timeState); + // create array of intervals (see Interval interface) + const intervalsArrays: Array = OverlaysService.createIntervalsArray(tf, timeState); + + // put each overlay in the right interval + overlays = overlays.sort(sortByDate); + let intervalIndex = 0; + overlays.forEach(o => { + // advance index until date is not exceeding interval end time + while (o.date > intervalsArrays[intervalIndex].endTime && intervalIndex < intervalsArrays.length) { + intervalIndex++; + } + const interval = intervalsArrays[intervalIndex]; + if (interval.startTime < o.date && o.date <= intervalsArrays[intervalIndex].endTime) { + interval.overlays.push(o); + } else { + console.warn(`overlay does not match any interval. id:${o.id}. date: ${o.date}`, o); + } + }); + + // return best-matching overlay by criteria + let overlaysByCriteria = OverlaysService.overlaysByCriteria(intervalsArrays, timeState.intervals.criteria); + + if (isDevMode()) { // log for debug purpose + console.table(intervalsArrays.map((obj, ind) => { + const match = overlaysByCriteria[ind]; + return { + startTime: obj.startTime.toISOString().replace('T', ' '), + endTime: obj.endTime.toISOString().replace('T', ' '), + pivot: obj.pivot.toISOString().replace('T', ' '), + overlays: obj.overlays.length, + match: match ? `id: ${match.id} time: ${match.date.toISOString().replace('T', ' ')}` : 'none', + by: timeState.intervals.criteria.type + }; + })); + } + + return overlaysByCriteria; + } + + // calculate TimeFrame object + static calculateTimeFrame(timeState: CaseTimeState): IntervalTimeFrame { + const intervalSize = timeState.intervals.interval; + const tf: IntervalTimeFrame = { + startDate: timeState.from, + endDate: timeState.to, + span: timeState.to.getTime() - timeState.from.getTime(), + intervalsCount: 0 + }; + + // calculate how many intervals + tf.intervalsCount = Math.floor(tf.span / intervalSize); + + // update time-frame (when time-frame % interval is not 0) + if (tf.span % intervalSize > 0) { + tf.startDate = new Date(tf.endDate.getTime() - (intervalSize * tf.intervalsCount)); + tf.span = tf.endDate.getTime() - tf.startDate.getTime(); + } + return tf; + } + + // create Array by timeFrame and timeState + static createIntervalsArray(timeFrame: IntervalTimeFrame, timeState: CaseTimeState): Array { + // set interval-size + const intervalSize = timeState.intervals.interval; + // initialize intervalsArrays + const startTime = timeFrame.startDate.getTime(); + const intervalsArrays = new Array(); + + doNTimes(timeFrame.intervalsCount, intervalIndex => { + let intervalStartMs = startTime + intervalIndex * intervalSize; + let intervalEndTimeMs = intervalStartMs + intervalSize - 1; + const intervalPivotTimeMs = intervalStartMs + intervalSize - 1; // pivot is always the end of the original scope + // manipulate interval according to criteria + switch (timeState.intervals.criteria.type) { + case 'best': + // move interval scope limits around pivot, according to before/after values + intervalStartMs = intervalPivotTimeMs - timeState.intervals.criteria.before; + intervalEndTimeMs = intervalPivotTimeMs + timeState.intervals.criteria.after; + break; + case 'closest-after': + // move interval scope limits so scope will start with pivot + intervalStartMs = intervalPivotTimeMs; + intervalEndTimeMs += intervalSize; + break; + case 'closest-both': + // move interval scope limits so pivot will be in the middle of scope + intervalStartMs += (intervalSize / 2); + intervalEndTimeMs += (intervalSize / 2); + break; + } + + intervalsArrays.push({ + startTime: new Date(intervalStartMs), + endTime: new Date(intervalEndTimeMs), + pivot: new Date(intervalPivotTimeMs), + overlays: [] + }) + }); + + return intervalsArrays; + } + // select best-matching overlay by criteria + static overlaysByCriteria(intervals: Array, criteria: CaseIntervalCriteria): Overlay[] { + const result: Overlay[] = []; + + // find the matching overlay for each interval by criteria + intervals.forEach(interval => { + let overlays = interval.overlays; + // const pivot = interval.pivot + if (overlays.length > 0) { + switch (criteria.type) { + case 'best': + // remove overlay outside of interval best-scope (best-before/best-after) + overlays = overlays.filter(o => { + const overlayTime: number = o.date.getTime(); + const intervalLow: number = interval.pivot.getTime() - criteria.before; + const intervalHigh: number = interval.pivot.getTime() + criteria.after; + return intervalLow <= overlayTime && overlayTime <= intervalHigh; + }).sort(sortByResolutionDesc); + break; + case 'closest-before': + overlays = overlays.filter(o => o.date.getTime() <= interval.pivot.getTime()) // remove overlays after pivot + .sort(sortByDateDesc); // sort - latest first + break; + case 'closest-after': + overlays = overlays.filter(o => o.date.getTime() >= interval.pivot.getTime()) // remove overlays before pivot + .sort(sortByDate); // sort - earliest first + break; + case 'closest-both': + overlays = overlays.sort((o1, o2) => // sort - closest + Math.abs(o1.date - interval.pivot.getTime()) - Math.abs(o1.date - interval.pivot.getTime())); + break; + } + if (overlays.length > 0) { + result.push(overlays[0]); + } + } + }); + + return result; + } + /* end */ + static isFullOverlay(overlay: Overlay): boolean { return Boolean(overlay && overlay.date); } @@ -67,17 +223,37 @@ export class OverlaysService { search(params: OverlaysCriteria): Observable { let feature = params.region; + const timeRange: Array = this.getSearchTimeRange(params.time); return this._overlaySourceProvider.fetch({ dataInputFilters: Boolean(params.dataInputFilters) && params.dataInputFilters.active ? params.dataInputFilters.filters : null, limit: this.config.limit, region: feature, - timeRange: { - start: params.time.from, - end: params.time.to - } + timeRange }); } + getSearchTimeRange(timeState: CaseTimeState): IDateRange[] { + const searchTimeRange: IDateRange[] = []; + + if (!timeState.intervals) { + searchTimeRange.push({ + start: timeState.from, + end: timeState.to + }); + } else { + const tf: IntervalTimeFrame = OverlaysService.calculateTimeFrame(timeState); + // create array of intervals (see Interval interface) + const intervalsArrays: Array = OverlaysService.createIntervalsArray(tf, timeState); + intervalsArrays.forEach(interval => { + searchTimeRange.push({ + start: interval.startTime, + end: interval.endTime + }); + }); + } + return searchTimeRange; + } + getOverlayById(id: string, sourceType: string): Observable { return this._overlaySourceProvider.getById(id, sourceType); } diff --git a/src/app/@ansyn/status-bar/components/combo-boxes/combo-boxes.component.html b/src/app/@ansyn/status-bar/components/combo-boxes/combo-boxes.component.html index 26b7fddf50..913b8a6384 100644 --- a/src/app/@ansyn/status-bar/components/combo-boxes/combo-boxes.component.html +++ b/src/app/@ansyn/status-bar/components/combo-boxes/combo-boxes.component.html @@ -86,7 +86,7 @@ + + + + diff --git a/src/app/@ansyn/status-bar/components/combo-boxes/combo-boxes.component.spec.ts b/src/app/@ansyn/status-bar/components/combo-boxes/combo-boxes.component.spec.ts index 1227a9a44f..1e7f069d86 100644 --- a/src/app/@ansyn/status-bar/components/combo-boxes/combo-boxes.component.spec.ts +++ b/src/app/@ansyn/status-bar/components/combo-boxes/combo-boxes.component.spec.ts @@ -12,6 +12,8 @@ import { IStatusBarState, statusBarFeatureKey, StatusBarReducer } from '../../re import { statusBarFlagsItemsEnum } from '@ansyn/status-bar/models/status-bar-flag-items.model'; import { UpdateStatusFlagsAction } from '@ansyn/status-bar/actions/status-bar.actions'; import { ClickOutsideDirective } from '@ansyn/status-bar/directives/click-outside.directive'; +import { TimelineIntervalsPickerComponent } from '@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component'; +import { DatePickerComponent } from '@ansyn/core/components/ansyn-date-picker/ansyn-date-picker.component'; describe('ComboBoxesComponent', () => { let component: ComboBoxesComponent; @@ -19,11 +21,13 @@ describe('ComboBoxesComponent', () => { const mockComboBoxComponent = MockComponent({ selector: 'ansyn-combo-box', inputs: ['options', 'selected', 'renderFunction', 'comboBoxToolTipDescription'], outputs: ['comboBoxToolTipDescription', 'selectedChange'] }); const ansynTreeView = MockComponent({ selector: 'ansyn-tree-view', outputs: ['closeTreeView'] }); const ansynComboTrigger = MockComponent({ selector: 'ansyn-combo-box-trigger', inputs: ['isActive', 'comboBoxToolTipDescription', 'render'] }); + const mockTimelineIntervalsPickerComponent = MockComponent({ selector: 'ansyn-timeline-intervals-picker', inputs: ['currentTimeState'] }); + let store: Store; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ComboBoxesComponent, mockComboBoxComponent, TimelineTimepickerComponent, ansynTreeView, ansynComboTrigger, ClickOutsideDirective], + declarations: [ComboBoxesComponent, mockComboBoxComponent, TimelineTimepickerComponent, mockTimelineIntervalsPickerComponent, ansynTreeView, ansynComboTrigger, ClickOutsideDirective], imports: [StoreModule.forRoot({ [coreFeatureKey]: CoreReducer, [statusBarFeatureKey]: StatusBarReducer }), EffectsModule.forRoot([])], providers: [ { diff --git a/src/app/@ansyn/status-bar/components/status-bar/status-bar.component.spec.ts b/src/app/@ansyn/status-bar/components/status-bar/status-bar.component.spec.ts index dd43c86612..62c9091e51 100644 --- a/src/app/@ansyn/status-bar/components/status-bar/status-bar.component.spec.ts +++ b/src/app/@ansyn/status-bar/components/status-bar/status-bar.component.spec.ts @@ -2,16 +2,7 @@ import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing' import { StatusBarComponent } from './status-bar.component'; import { Store, StoreModule } from '@ngrx/store'; import { IStatusBarState, statusBarFeatureKey, StatusBarReducer } from '../../reducers/status-bar.reducer'; -import { ExpandAction, UpdateStatusFlagsAction } from '../../actions/status-bar.actions'; -import { StatusBarModule } from '../../status-bar.module'; import { EffectsModule } from '@ngrx/effects'; -import { LoggerConfig } from '@ansyn/core/models/logger.config'; -import { StatusBarConfig } from '../../models/statusBar.config'; -import { statusBarFlagsItemsEnum } from '../../models/status-bar-flag-items.model'; -import { GoAdjacentOverlay } from '@ansyn/core/actions/core.actions'; -import { comboBoxesOptions, GEO_FILTERS, ORIENTATIONS, TIME_FILTERS } from '../../models/combo-boxes.model'; -import { ALERTS } from '@ansyn/core/alerts/alerts.model'; -import { ImageryStatusComponent } from '@ansyn/core/components/imagery-status/imagery-status.component'; import { MockComponent } from '@ansyn/core/test/mock-component'; import { coreFeatureKey, CoreReducer } from '@ansyn/core/reducers/core.reducer'; @@ -19,14 +10,24 @@ describe('StatusBarComponent', () => { let component: StatusBarComponent; let fixture: ComponentFixture; let store: Store; - const mockImageryStatusComponent = MockComponent({ selector: 'ansyn-imagery-status', inputs: ['overlay', 'active', 'mapId'], outputs: ['backToWorldView']}) - const mockSelectedCaseBarComponent = MockComponent({ selector: 'ansyn-selected-case-bar', inputs: ['selectedCaseName'] }); + const mockImageryStatusComponent = MockComponent({ + selector: 'ansyn-imagery-status', + inputs: ['overlay', 'active', 'mapId'], + outputs: ['backToWorldView'] + }); + const mockSelectedCaseBarComponent = MockComponent({ + selector: 'ansyn-selected-case-bar', + inputs: ['selectedCaseName'] + }); const mockComboBoxes = MockComponent({ selector: 'ansyn-combo-boxes' }); const mockNavigationBar = MockComponent({ selector: 'ansyn-navigation-bar' }); beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [StoreModule.forRoot({ [statusBarFeatureKey]: StatusBarReducer, [coreFeatureKey]: CoreReducer }), EffectsModule.forRoot([])], + imports: [StoreModule.forRoot({ + [statusBarFeatureKey]: StatusBarReducer, + [coreFeatureKey]: CoreReducer + }), EffectsModule.forRoot([])], declarations: [StatusBarComponent, /* mock */ mockImageryStatusComponent, diff --git a/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options-interval.pipe.spec.ts b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options-interval.pipe.spec.ts new file mode 100644 index 0000000000..bbab61280a --- /dev/null +++ b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options-interval.pipe.spec.ts @@ -0,0 +1,8 @@ +import { TimeOptionsIntervalPipe } from './time-options-interval.pipe'; + +describe('TimeOptionsIntervalPipe', () => { + it('create an instance', () => { + const pipe = new TimeOptionsIntervalPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options-interval.pipe.ts b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options-interval.pipe.ts new file mode 100644 index 0000000000..350bc381fa --- /dev/null +++ b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options-interval.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'timeOptionsInterval' +}) +export class TimeOptionsIntervalPipe implements PipeTransform { + + transform(value: any, args?: any): any { + const index = value.findIndex(({ value }) => value === Number(args)); + return value.splice(0, index); + } + +} diff --git a/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options.pipe.spec.ts b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options.pipe.spec.ts new file mode 100644 index 0000000000..740169759e --- /dev/null +++ b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options.pipe.spec.ts @@ -0,0 +1,8 @@ +import { TimeOptionsPipe } from './time-options.pipe'; + +describe('TimeOptionsPipe', () => { + it('create an instance', () => { + const pipe = new TimeOptionsPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options.pipe.ts b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options.pipe.ts new file mode 100644 index 0000000000..b822f04b91 --- /dev/null +++ b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/time-options.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'timeOptions' +}) +export class TimeOptionsPipe implements PipeTransform { + + transform(value: any, args?: any): any { + const index = value.findIndex(({ value }) => value === Number(args)); + return value.slice(index + 1, value.length); + } + +} diff --git a/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.html b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.html new file mode 100644 index 0000000000..c0fd5e1289 --- /dev/null +++ b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.html @@ -0,0 +1,176 @@ +
+ +
+
Each:
+ +
+ +
+
For last:
+ +
+ +
+ + + + + + + + + + + + + + + + + + +
+
+
+ Time Settings +
+
+ +
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+
+
+ Time Frame: +
+
    +
  • + + + + + +
  • +
  • + +
    +
    + + + +
    +
    + + + +
    +
    +
  • +
  • +

    {{validation.timeFrameErr}}

    +
  • +
+
+
+ Criteria: +
+
+
+ + +
+ +
+
+ + + Before +
+ +
+ + After +
+
+
+ +
+ + + + +
+ +
+
+ +
+

{{validation.BestWithinErr}}

+
+ +
+
+ +
+
+ {{error}} +
+
+ Cancel + Ok +
+
diff --git a/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.less b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.less new file mode 100644 index 0000000000..21c9966ef5 --- /dev/null +++ b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.less @@ -0,0 +1,164 @@ +@import "core/less/common"; + +@vertical-section-margin: 8px 0; +@row-bottom-margin: 5px; + +:host { + color: white; + position: absolute; + display: flex; + box-shadow: 0 0 2px 1px white; + background: black; + padding: 5px 25px; + flex-direction: column; + top: 0; + right: 0; + transform: translate(0, calc(~'-100% - 15px')); + + label, input[type=radio], select { + cursor: pointer; + padding: 2px; + } + input[type=number] { + width: 35px; + padding: 2px; + } + ul { + padding: 0; + margin: 0; + li { + list-style-type: none; + margin-bottom: 5px; + } + } + .disabled, input:disabled, input:disabled + label, select:disabled { + color: @text-disabled-color; + cursor: auto; + } + .row { + margin-bottom: @row-bottom-margin; + display: flex; + } + .no-bottom-margin { + margin-bottom: 0; + } + + div.basic-options-container { + .select-box { + display: flex; + align-items: center; + margin: 6px 0; + .text { + color: white; + margin-right: 10px; + width: 50px; + display: inline-block; + white-space: pre; + } + select { + width: 70px; + } + } + } + + button.advance-options-button { + color: white; + background: none; + border: 0; + outline: 0; + padding: 0; + display: flex; + justify-content: flex-end; + .chevron { + margin-left: 4px; + display: inline-block; + font-size: 22px; + line-height: 14px; + transition: transform .3s linear; + + } + &.active .chevron { + transform: rotate(90deg); + } + } + + .advance-options-container { + max-height: 0; + overflow: hidden; + transition: max-height .4s linear 0s; + &.active { + max-height: 500px; + } + + .advance-options-time { + .advance-options-time-intervals { + margin: @vertical-section-margin; + label { + white-space: nowrap; + } + ul { + display: flex; + li { + display: flex; + flex-direction: column; + margin: 0 5px; + text-align: center; + } + } + } + .advance-options-time-frame { + margin: @vertical-section-margin; + label { + min-width: 50px; + } + } + } + .advance-options-strategies { + > div { + margin: @vertical-section-margin; + &:first-child { + margin-bottom: 10px; + } + } + label { + min-width: 80px; + } + input[type=radio] { + margin-left: 0; + } + select { + min-width: 90px; + } + .filter-strategy-best span { + margin-left: 5px; + } + } + } + + div.control-buttons { + margin-top: 10px; + display: flex; + justify-content: space-between; + color: @overlays-dots-color; + a { + cursor: pointer; + &:hover { + font-weight: bold; + } + &:first-child { + margin-right: 9px; + width: 42px; + } + &:last-child { + width: 17px; + } + } + .error { + color: crimson; + } + + } +} + + + diff --git a/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.spec.ts b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.spec.ts new file mode 100644 index 0000000000..1409d8faef --- /dev/null +++ b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.spec.ts @@ -0,0 +1,27 @@ +import { TimelineIntervalsPickerComponent } from './timeline-intervals-picker.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; + +describe('TimelineIntervalsPickerComponent', () => { + let component: TimelineIntervalsPickerComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TimelineIntervalsPickerComponent], + imports: [ + FormsModule + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimelineIntervalsPickerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('Component Initialised ', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.ts b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.ts new file mode 100644 index 0000000000..44a6b320e5 --- /dev/null +++ b/src/app/@ansyn/status-bar/components/timeline-intervals-picker/timeline-intervals-picker.component.ts @@ -0,0 +1,274 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { IDateRange } from '@ansyn/core/models/time.model'; +import { CaseIntervalCriteria, CaseIntervalCriteriaType, CaseTimeState } from '@ansyn/core/models/case.model'; + + + +const HOUR_MILLISECONDS = 60 * 60 * 1000; + +@Component({ + selector: 'ansyn-timeline-intervals-picker', + templateUrl: './timeline-intervals-picker.component.html', + styleUrls: ['./timeline-intervals-picker.component.less'] +}) + +export class TimelineIntervalsPickerComponent { + public timeIntervalAdvancedOptions = false; + public timeFrame: 'last' | 'scope' = 'last'; + public timeIntervalFilterStrategy: 'best' | 'closest' = 'best'; + public timeFilterUnits = { + options: [ + { text: 'hours', textSingular: 'hour', value: HOUR_MILLISECONDS }, + { text: 'days', textSingular: 'day', value: HOUR_MILLISECONDS * 24 }, + { text: 'months', textSingular: 'month', value: HOUR_MILLISECONDS * 24 * 30 }, + { text: 'years', textSingular: 'year', value: HOUR_MILLISECONDS * 24 * 30 * 12 } + ], + map: new Map() + }; + // hour, 5 hours, 12 hours, 2 days, 3 days, week, month, 2 months. + public bestWithin = { + options: [ + { text: 'hour', valid: true, value: HOUR_MILLISECONDS }, + { text: '5 hours', valid: true, value: HOUR_MILLISECONDS * 5 }, + { text: '12 hours', valid: true, value: HOUR_MILLISECONDS * 12 }, + { text: '2 days', valid: true, value: HOUR_MILLISECONDS * 24 * 2}, + { text: '3 days', valid: true, value: HOUR_MILLISECONDS * 24 * 3 }, + { text: 'week', valid: true, value: HOUR_MILLISECONDS * 24 * 7 }, + { text: 'month', valid: true, value: HOUR_MILLISECONDS * 24 * 30 }, + { text: '2 months', valid: true, value: HOUR_MILLISECONDS * 24 * 30 * 2 } + ], + validOptions: [], + }; + + public validation = { + timeFrameErr: null, + BestWithinErr: null, + reset: function () { + this.timeFrameErr = null; + this.BestWithinErr = null; + }, + isValid: function () { + return this.timeFrameErr === null && this.BestWithinErr === null; + } + }; + + public basicOptionInterval: number; + public basicOptionTimeFrame: number; + public advancedOptionInterval = {years: 0, months: 0, days: 0 , hours: 0 }; + public advancedOptionTimeFrameLast: number; + public advancedOptionTimeFrameUnits: number; + public advancedOptionStartDate = new Date(new Date().getTime() - HOUR_MILLISECONDS * 24 * 365); // default is 1 yr ago + public advancedOptionEndDate = new Date(); + public advancedOptionCriteriaBest = { before: 0, after: 0}; + public advancedOptionCriteriaClosest = 'closest-before'; + + private _currentTimeState: CaseTimeState; + @Input() + set currentTimeState(value) { + this._currentTimeState = value; + if (this._currentTimeState && this._currentTimeState.intervals) { + this.loadCurrentTimeState(); + } + }; + @Output() applyDate = new EventEmitter(); + @Output() closeComponent = new EventEmitter(); + + constructor() { + // init map for easy search in timeFilterUnits.options + this.timeFilterUnits.options.forEach(unit => { + this.timeFilterUnits.map.set(unit.text, unit.value); + this.timeFilterUnits.map.set(unit.textSingular, unit.value); + this.timeFilterUnits.map.set(unit.value, unit.text); + }); + // init values + this.loadDefaultValues(); + } + + // load data from state + loadCurrentTimeState() { + const interval: number = this._currentTimeState.intervals.interval; + const timeFrame = this._currentTimeState.to.getTime() - this._currentTimeState.from.getTime(); + const criteria: CaseIntervalCriteria = this._currentTimeState.intervals.criteria; + + const inBasicMode = this.timeFilterUnits.map.get(interval) && + this.timeFilterUnits.map.get(interval) && + criteria.type === 'closest-both'; + + if (inBasicMode) { + this.basicOptionInterval = interval; + this.basicOptionTimeFrame = timeFrame; + } + + this.timeIntervalAdvancedOptions = !inBasicMode; + this.loadAdvancedOptionInterval(interval); + this.loadAdvancedOptionTimeFrame(this._currentTimeState.from, this._currentTimeState.to); + this.loadAdvancedOptionCriteria(criteria); + + this.timeIntervalAdvancedOptions = !inBasicMode; + this.calculateBestWithin(); + } + // load data from state - interval + loadAdvancedOptionInterval(interval: number) { + this.advancedOptionInterval.years = Math.floor(interval / this.timeFilterUnits.map.get('year')); + this.advancedOptionInterval.months = Math.floor(interval / this.timeFilterUnits.map.get('month')) % 12; + this.advancedOptionInterval.days = Math.floor(interval / this.timeFilterUnits.map.get('day')) % 30; + this.advancedOptionInterval.hours = Math.floor(interval / this.timeFilterUnits.map.get('hour')) % 24; + } + // load data from state - time frame + loadAdvancedOptionTimeFrame(from: Date, to: Date) { + this.advancedOptionStartDate = from; + this.advancedOptionEndDate = to; + this.setTimeFrameType('scope'); + } + // load data from state - criteria + loadAdvancedOptionCriteria(criteria: CaseIntervalCriteria) { + if (criteria.type === 'best') { + this.advancedOptionCriteriaBest.before = criteria.before; + this.advancedOptionCriteriaBest.after = criteria.after; + this.timeIntervalFilterStrategy = 'best'; + } else { + this.timeIntervalFilterStrategy = 'closest'; + this.advancedOptionCriteriaClosest = criteria.type; + } + } + + loadDefaultValues() { + this.basicOptionInterval = this.timeFilterUnits.map.get('month'); + this.basicOptionTimeFrame = this.timeFilterUnits.map.get('year'); + this.advancedOptionInterval.months = 1; + this.advancedOptionTimeFrameLast = 1; + this.advancedOptionTimeFrameUnits = this.timeFilterUnits.map.get('year'); + this.advancedOptionCriteriaBest.before = this.timeFilterUnits.map.get('hour'); + this.advancedOptionCriteriaBest.after = this.timeFilterUnits.map.get('hour'); + this.calculateBestWithin(); + } + + calculateBestWithin() { + const interval: number = this.getTimeInterval(); + this.bestWithin.validOptions = []; + + this.bestWithin.options.forEach(o => { + if (o.value < interval) { + this.bestWithin.validOptions.push(o); + } + }); + } + + toggleIntervalAdvancedOptions() { + this.timeIntervalAdvancedOptions = !this.timeIntervalAdvancedOptions; + this.calculateBestWithin(); + } + + setTimeFrameType(type) { + this.timeFrame = type; + } + + setIntervalsFilterStrategy(type) { + this.timeIntervalFilterStrategy = type; + } + + checkValidation(): boolean { + const interval: number = this.getTimeInterval(); + const timeFrame = this.getTimeFrame(); + const criteria: CaseIntervalCriteria = this.getCriteria(); + + this.validation.reset(); + + // check time frame + const timeFrameMs = timeFrame.end.getTime() - timeFrame.start.getTime(); + + if (timeFrameMs <= 0) { + this.validation.timeFrameErr = 'Start time exceeds End time'; + } else if (timeFrameMs <= interval) { + this.validation.timeFrameErr = 'Time frame smaller than interval size'; + } + + // check best within + // both before and after property exist and have number values + if (!isNaN(criteria.before + criteria.after)) { + if ((criteria.before + criteria.after) > interval) { + this.validation.BestWithinErr = 'Criteria range bigger exceeds interval size'; + } + } + + return this.validation.isValid(); + } + + applyIntervalPickerEvent() { + if (!this.checkValidation()) { + return; + } + + const interval: number = this.getTimeInterval(); + const timeFrame = this.getTimeFrame(); + const criteria: CaseIntervalCriteria = this.getCriteria(); + const intervalsData: CaseTimeState = { + type: 'absolute', + from: timeFrame.start, + to: timeFrame.end, + intervals: { + interval, + criteria + } + }; + + this.applyDate.emit(intervalsData); + } + + getTimeFrame(): IDateRange { + let start; + let end = new Date(); // default is 'now' + + // basic mode + if (this.timeIntervalAdvancedOptions === false) { + start = new Date(end.getTime() - this.basicOptionTimeFrame); + } else { + // advanced mode + if (this.timeFrame === 'last') { + // advanced mode: time frame > last + start = new Date(end.getTime() - this.advancedOptionTimeFrameLast * this.advancedOptionTimeFrameUnits); + } else { + // advanced mode: time frame > start - end + start = this.advancedOptionStartDate; + end = this.advancedOptionEndDate; + } + } + + return { start, end }; + } + + getTimeInterval(): number { + let interval; + // basic mode + if (this.timeIntervalAdvancedOptions === false) { + interval = this.basicOptionInterval; + } else { + // advanced mode + interval = + this.advancedOptionInterval.years * this.timeFilterUnits.map.get('years') + + this.advancedOptionInterval.months * this.timeFilterUnits.map.get('months') + + this.advancedOptionInterval.days * this.timeFilterUnits.map.get('days') + + this.advancedOptionInterval.hours * this.timeFilterUnits.map.get('hours'); + } + return +interval; + } + + getCriteria(): CaseIntervalCriteria { + let criteria: CaseIntervalCriteria = { + type: 'closest-both' // default for basic mode + }; + // advanced mode + if (this.timeIntervalAdvancedOptions) { + if (this.timeIntervalFilterStrategy === 'best') { + // advanced mode: Criteria > best + criteria.type = 'best'; + criteria.before = +this.advancedOptionCriteriaBest.before; + criteria.after = +this.advancedOptionCriteriaBest.after; + } else { + // advanced mode: Criteria > closest + criteria.type = this.advancedOptionCriteriaClosest; + } + } + return criteria; + } +} diff --git a/src/app/@ansyn/status-bar/components/timeline-timepicker/timeline-timepicker.component.less b/src/app/@ansyn/status-bar/components/timeline-timepicker/timeline-timepicker.component.less index 5b129d5d82..fc83b5c2e8 100644 --- a/src/app/@ansyn/status-bar/components/timeline-timepicker/timeline-timepicker.component.less +++ b/src/app/@ansyn/status-bar/components/timeline-timepicker/timeline-timepicker.component.less @@ -2,7 +2,6 @@ :host { color: white; - transition: .4s; position: absolute; display: flex; box-shadow: 0 0 2px 1px white; diff --git a/src/app/@ansyn/status-bar/models/combo-boxes.model.ts b/src/app/@ansyn/status-bar/models/combo-boxes.model.ts index f9a2d3b086..47b94ab795 100644 --- a/src/app/@ansyn/status-bar/models/combo-boxes.model.ts +++ b/src/app/@ansyn/status-bar/models/combo-boxes.model.ts @@ -1,8 +1,8 @@ import { CaseGeoFilter, CaseOrientation, CaseTimeFilter } from '@ansyn/core/models/case.model'; import { InjectionToken } from '@angular/core'; -export const timeFilters: CaseTimeFilter[] = ['Start - End']; -export const geoFilters: CaseGeoFilter[] = Object.values(CaseGeoFilter); +export const geoFilters: CaseGeoFilter[] = Object.values(CaseGeoFilter); +export const timeFilters: CaseTimeFilter[] = Object.values(CaseTimeFilter); export const orientations: CaseOrientation[] = ['Align North', 'User Perspective', 'Imagery Perspective']; export const ORIENTATIONS = new InjectionToken('ORIENTATIONS'); diff --git a/src/app/@ansyn/status-bar/status-bar.module.ts b/src/app/@ansyn/status-bar/status-bar.module.ts index 225fed92cd..9b98fd9b20 100644 --- a/src/app/@ansyn/status-bar/status-bar.module.ts +++ b/src/app/@ansyn/status-bar/status-bar.module.ts @@ -5,6 +5,7 @@ import { FormsModule } from '@angular/forms'; import { CoreModule } from '@ansyn/core/core.module'; import { ComboBoxComponent } from './components/combo-box/combo-box.component'; import { TimelineTimepickerComponent } from './components/timeline-timepicker/timeline-timepicker.component'; +import { TimelineIntervalsPickerComponent } from './components/timeline-intervals-picker/timeline-intervals-picker.component'; import { StoreModule } from '@ngrx/store'; import { statusBarFeatureKey, StatusBarReducer } from './reducers/status-bar.reducer'; import { @@ -20,6 +21,8 @@ import { ComboBoxesComponent } from './components/combo-boxes/combo-boxes.compon import { SelectedCaseBarComponent } from './components/selected-case-bar/selected-case-bar.component'; import { ComboBoxTriggerComponent } from './components/combo-box-trigger/combo-box-trigger.component'; import { ClickOutsideDirective } from './directives/click-outside.directive'; +import { TimeOptionsPipe } from './components/timeline-intervals-picker/time-options.pipe'; +import { TimeOptionsIntervalPipe } from './components/timeline-intervals-picker/time-options-interval.pipe'; @NgModule({ @@ -30,7 +33,20 @@ import { ClickOutsideDirective } from './directives/click-outside.directive'; TreeviewModule.forRoot(), StoreModule.forFeature(statusBarFeatureKey, StatusBarReducer) ], - declarations: [StatusBarComponent, ComboBoxComponent, TimelineTimepickerComponent, TreeViewComponent, NavigationBarComponent, ComboBoxesComponent, SelectedCaseBarComponent, ComboBoxTriggerComponent, ClickOutsideDirective], + declarations: [ + StatusBarComponent, + ComboBoxComponent, + TimelineTimepickerComponent, + TreeViewComponent, + NavigationBarComponent, + ComboBoxesComponent, + SelectedCaseBarComponent, + ComboBoxTriggerComponent, + ClickOutsideDirective, + TimelineIntervalsPickerComponent, + TimeOptionsPipe, + TimeOptionsIntervalPipe + ], providers: [ { provide: ORIENTATIONS,