diff --git a/src/main/js/apps/sample/app.json b/src/main/js/apps/sample/app.json index 893dd29..0165f99 100644 --- a/src/main/js/apps/sample/app.json +++ b/src/main/js/apps/sample/app.json @@ -72,6 +72,19 @@ "componentEnabled": true } }, + "toc": { + "Config": { + "actions": [ + "show-description", + "zoom-to-extent", + "activate-children", + "deactivate-children", + "change-opacity", + "show-copyright", + "timeslider" + ] + } + }, "banner": { "BannerWidget": { "label": "Developer Network", @@ -99,8 +112,40 @@ { "id": "buchdrucker", "title": "Gefährdung Buchdrucker", - "type": "AGS_FEATURE", - "url": "https://www.fovgis.bayern.de/arcgis/rest/services/baywis_wsm/borki_gef/FeatureServer/1" + "type": "AGS_DYNAMIC", + "url": "https://www.fovgis.bayern.de/arcgis/rest/services/baywis_wsm/borki_gef/MapServer", + "listMode": "hide-children", + "sublayers": [ + { + "id": 1, + "visible": true + } + ], + "timeSlider": { + "timeExtent": { + "start": "2019-04-15T00:00Z", + "end": "2019-04-15T00:00Z" + }, + "fullTimeExtent": { + "start": "2019-01-01T00:00Z", + "end": "2019-12-31T00:00Z" + }, + "stops": { + "interval": { + "value": 1, + "unit": "weeks" + }, + "timeExtent": { + "start": "2019-04-01T00:00Z", + "end": "2019-10-06T00:00Z" + } + }, + "mode": "instant", + "loop": true, + "playRate": 1000, + "playOnStartup": true, + "timeVisible": false + } }, { "id": "kupferstecher", @@ -229,7 +274,8 @@ "toolrules": { "ToolActiveStateManager": { "activateOnStartToolIds": [ - "timeSliderToggleTool", + // "timeSliderToggleTool", + "tocToggleTool", "legendToggleTool" ] } diff --git a/src/main/js/bundles/dn_timeslider/README.md b/src/main/js/bundles/dn_timeslider/README.md index 904dbf1..6e4b830 100644 --- a/src/main/js/bundles/dn_timeslider/README.md +++ b/src/main/js/bundles/dn_timeslider/README.md @@ -1,26 +1,40 @@ # dn_timeslider -The Time Slider bundle allows the user to change the time extent of the map. +The Time Slider bundle allows the user to change the time extent of the map or specific layers. ## Usage **Requirement: map.apps 4.12.0** -1. First you need to add the bundle dn_timeslider to your app. -2. Then you need to configure it. - -To make the functions of this bundle available to the user, the following tool can be added to a toolset: - -| Tool ID | Component | Description | -| -------------------- | -------------------- | ------------------------ | -| timeSliderToggleTool | TimeSliderToggleTool | Show or hide the widget. | +
    +
  1. First you need to add the bundle dn_timeslider to your app.
  2. +
  3. Then you need to configure it. This can be accomplished in two separate yet complementary ways: + +
  4. +
## Configuration Reference ### Config -#### Sample configuration +#### Sample configuration for map Time Slider ```json "Config": { + "timeExtent": { + "start": "2019-04-15T00:00Z", + "end": "2019-04-15T00:00Z" + }, "fullTimeExtent": { "start": "2019-01-01T00:00Z", "end": "2019-12-31T00:00Z" @@ -44,6 +58,50 @@ To make the functions of this bundle available to the user, the following tool c } ``` +#### Sample configuration for layer Time Slider +```json +"map-init": { + "Config": { + "map": { + "layers": [ + { + "id": "buchdrucker", + "title": "Gefährdung Buchdrucker", + "type": "AGS_FEATURE", + "url": "https://www.fovgis.bayern.de/arcgis/rest/services/baywis_wsm/borki_gef/FeatureServer/1", + "timeSlider": { + "timeExtent": { + "start": "2019-04-15T00:00Z", + "end": "2019-04-15T00:00Z" + }, + "fullTimeExtent": { + "start": "2019-01-01T00:00Z", + "end": "2019-12-31T00:00Z" + }, + "viewTimeExtent": null, + "stops": { + "interval": { + "value": 1, + "unit": "weeks" + }, + "timeExtent": { + "start": "2019-03-01T00:00Z", + "end": "2019-09-01T00:00Z" + } + }, + "mode": "instant", + "loop": true, + "playRate": 1000, + "playOnStartup": true, + "timeVisible": false + } + } + ] + } + } +} +``` + | Property | Type | Possible Values | Default | Description | | -------------- | ------- | ---------------------------------------------------------------------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | timeExtent | Object | | | The initial temporal extent of the slider. More information is available in the [TimeSlider](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-TimeSlider-TimeSliderViewModel.html#timeExtent) documentation. | @@ -56,6 +114,8 @@ To make the functions of this bundle available to the user, the following tool c | playOnStartup | Boolean | ```true``` | ```false``` | ```false``` | When true, the time slider will play its animation on startup. | | timeVisible | Boolean | ```true``` | ```false``` | ```false``` | Shows/hides time in the display. | + + #### Configuration of fullTimeExtent To configure this property you need to define a start and end date. To do this you can use [Moment.js-Strings](https://momentjs.com/docs/#/parsing/). @@ -263,5 +323,5 @@ If you configure the stops over an interval or a number, you have the additional ### Configuration of labels -To change the format and styling of the labels, you can inject a FormatFunction with the Interface `"dn_timeslider.LabelFormatFunction"`. It allows to change the visual representation of the labels inside of the TimeSlider. +To change the format and styling of the labels, you can inject a FormatFunction with the Interface `"dn_timeslider.LabelFormatFunction"`. It allows to change the visual representation of the labels inside of the TimeSlider. More information is available in the [TimeSlider::labelFormatFunction](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-TimeSlider.html#labelFormatFunction) documentation. diff --git a/src/main/js/bundles/dn_timeslider/TimeSliderTocActionDefinitionFactory.ts b/src/main/js/bundles/dn_timeslider/TimeSliderTocActionDefinitionFactory.ts new file mode 100644 index 0000000..ed8c113 --- /dev/null +++ b/src/main/js/bundles/dn_timeslider/TimeSliderTocActionDefinitionFactory.ts @@ -0,0 +1,112 @@ +/// +/// Copyright (C) 2023 con terra GmbH (info@conterra.de) +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import type { InjectedReference } from "apprt-core/InjectedReference"; +import ct_util from "ct/ui/desktop/util"; +import async from "apprt-core/async"; +import EsriDijit from "esri-widgets/EsriDijit"; + +import { MessagesReference } from "./nls/bundle"; +import type TimeSliderWidgetController from "./TimeSliderWidgetController"; +import { ExtendedLayer } from "../../types/ExtendedLayer"; + +export default class TimeSliderTocActionDefinitionFactory { + public delay = 1000; + public supportedIds: Array; + + private Id = "timeslider"; + private bundleContext: InjectedReference = undefined; + private timeExtentWatcher: InjectedReference = undefined; + private serviceRegistration: InjectedReference = undefined; + private _i18n: InjectedReference; + private _timeSliderWidgetController: TimeSliderWidgetController; + + public activate(componentContext: InjectedReference): void { + this.bundleContext = componentContext.getBundleContext(); + } + + public constructor() { + this.supportedIds = [this.Id]; + } + + public createDefinitionById(id: string): any { + if (id !== this.Id) { + return; + } + const i18n = this._i18n.get(); + const timeSliderWidgetController = this._timeSliderWidgetController; + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this; + + return { + id: "timeslider", + type: "button", + label: i18n.tocActionLabel, + icon: "icon-time-forward", + + isVisibleForItem(tocItem: any) { + const ref = tocItem.ref; + return typeof ref.timeInfo !== "undefined" && ref.timeInfo !== null && !!ref.timeSlider; + }, + + trigger(tocItem: any) { + const layer = tocItem.ref; + const controller = timeSliderWidgetController; + const timeSliderProperties = tocItem.ref.timeSlider; + + layer.visible = true; + + if (layer.timeExtent) { + timeSliderProperties.timeExtent = layer.timeExtent; + timeSliderProperties.fullTimeExtent = layer.timeInfo.fullTimeExtent; + timeSliderProperties.stops = layer.stops; + } + + const timeSliderWidget = controller.getWidget(timeSliderProperties); + that.timeExtentWatcher = timeSliderWidget.watch("timeExtent", (value) => { + layer.timeExtent = value; + }); + this.supressLayerDefaults(layer, timeSliderProperties, timeSliderWidget); + const widget = new (EsriDijit as any)(timeSliderWidget); + const serviceProperties = { + "widgetRole": "layerTimeSliderWidget" + }; + const interfaces = ["dijit.Widget"]; + that.serviceRegistration = that.bundleContext.registerService(interfaces, widget, serviceProperties); + + async(() => { + const window = ct_util.findEnclosingWindow(timeSliderWidget); + window.set("title", `${window.title} - ${layer.title}`); + window?.on("Hide", () => { + that.timeExtentWatcher.remove(); + that.timeExtentWatcher = undefined; + }); + }, that.delay); + }, + + supressLayerDefaults(layer: ExtendedLayer, props: InjectedReference>, widget: any) { + if (props) { + layer.timeInfo.fullTimeExtent = props.fullTimeExtent; + layer.stops = props.stops; + } else if (widget.fullTimeExtent) { + layer.timeInfo.fullTimeExtent = widget.fullTimeExtent; + layer.stops = widget.stops; + } + } + }; + } + +} diff --git a/src/main/js/bundles/dn_timeslider/TimeSliderWidgetController.js b/src/main/js/bundles/dn_timeslider/TimeSliderWidgetController.js deleted file mode 100644 index 3d6ec20..0000000 --- a/src/main/js/bundles/dn_timeslider/TimeSliderWidgetController.js +++ /dev/null @@ -1,418 +0,0 @@ -/* - * Copyright (C) 2023 con terra GmbH (info@conterra.de) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import TimeSlider from "esri/widgets/TimeSlider"; -import moment from "moment"; - -export default class TimeSliderWidgetFactory { - - #timeSliderWidget = undefined; - #timeExtentWatcher = undefined; - #initialTimeExtent = undefined; - - /** - * A function used to specify custom formatting and styling - * @type __esri.DateLabelFormatter - */ - #labelFormatFunction = undefined; - - activate() { - this._getView().then((view) => { - // check for viewTimeExtent property; if undefined don't set new view's timeExtent - if (this._properties.viewTimeExtent) { - view.timeExtent = this._getViewTimeExtent(); - } - this.#initialTimeExtent = view.timeExtent; - }); - } - - deactivate() { - this._resetTimeExtent(); - this._destroyWidget(); - } - - /** - * Sets formatter for min, max and extent labels of the TimeSlider - * Injected via OSGI - * @param {__esri.DateLabelFormatter} labelFormatFunction A function used to specify custom formatting and styling - * @public - */ - setLabelFormatFunction(labelFormatFunction) { - this.#labelFormatFunction = labelFormatFunction; - } - - /** - * Gets called when the tool gets activated - */ - onToolActivated() { - this._getView().then((view) => { - view.timeExtent = this.#timeSliderWidget.timeExtent; - this._changeAllLayerTimeExtents(view.timeExtent); - if (this._properties.playOnStartup) { - this.#timeSliderWidget.play(); - } - this.#timeExtentWatcher = this.#timeSliderWidget.watch("timeExtent", (value) => { - this._changeAllLayerTimeExtents(value); - }); - }); - } - - /** - * Gets called when the tool gets deactivated - */ - onToolDeactivated() { - this.#timeSliderWidget.stop(); - this.#timeExtentWatcher.remove(); - this._resetAllLayerTimeExtents(); - this._resetTimeExtent(); - } - - /** - * Returns the TimeSliderWidget - * - * @returns {TimeSlider} - */ - getWidget() { - const timeSliderProperties = this._getTimeSliderProperties(); - return this.#timeSliderWidget = new TimeSlider(timeSliderProperties); - } - - _destroyWidget() { - this.#timeSliderWidget.destroy(); - this.#timeSliderWidget = undefined; - } - - /** - * Generated the TimeSlider configuration - * - * @returns {*} TimeSlider configuration - */ - _getTimeSliderProperties() { - const properties = this._properties; - const timeSliderProperties = { - timeExtent: this._getInitialTimeExtent(), - fullTimeExtent: this._getFullTimeExtent(), - mode: properties.mode, - loop: properties.loop, - playRate: properties.playRate, - timeVisible: properties.timeVisible - }; - const stops = this._getStops(); - if (stops) { - timeSliderProperties.stops = stops; - } - - if (this.#labelFormatFunction) { - // use formatter in widget - timeSliderProperties.labelFormatFunction = this.#labelFormatFunction; - } - - return timeSliderProperties; - } - - /** - * Generates the initial values for the TimeSlider - * - * @returns {Array} Values for the TimeSlider - * @private - */ - _getInitialTimeExtent() { - const properties = this._properties; - const timeExtent = properties.timeExtent; - - return this._getTimeExtent(timeExtent); - } - - /** - * Function used to access fullTimeExtent properties and call _getTimeExtent() - * - * @returns {{start, end}} An object with start and end date - * @private - */ - _getFullTimeExtent() { - const properties = this._properties; - const fullTimeExtent = properties.fullTimeExtent; - - return this._getTimeExtent(fullTimeExtent); - } - - /** - * Function used to access viewTimeExtent properties and call _getTimeExtent() - * - * @returns {{start, end}} An object with start and end date - * @private - */ - _getViewTimeExtent() { - const properties = this._properties; - const viewTimeExtent = properties.viewTimeExtent; - return this._getTimeExtent(viewTimeExtent); - } - - /** - * Function used to pass start and end component of timeExtent seperatly to _constructMoment() - * - * @param {Object} referenceTimeExtent Object representing timeExtent; Contains start and end property - * @returns {{start, end}} Object containing two dates, constructed using _constructMoment() - * @private - */ - _getTimeExtent(referenceTimeExtent) { - const start = this._constructDate(referenceTimeExtent.start); - const end = this._constructDate(referenceTimeExtent.end); - - return { - start: start, - end: end - }; - } - - /** - * Function used to construct a moment from properties defined in manifest.json/app.json - * @param {Object} referenceMoment Moment properties extracted from referenceTimeExtent - * @returns Moment constructed according to parameters - * @private - */ - _constructDate(referenceMoment) { - let momentObj = moment.utc(); - // Case: Property is either "now", undefined or null - if (referenceMoment === "now" || !referenceMoment) { - // do nothing because moment object has already the current date - } else if (Array.isArray(referenceMoment)) { - referenceMoment.forEach((m, i) => { - // Case: Property is an array and leading element is a string - if (typeof m === 'string' && i === 0) { - momentObj = moment(m).utc(); - if (momentObj && !momentObj.isValid()) { - momentObj = moment().utc(); - console.warn("Invalid timeExtent definition. Use current time."); - } - } else { - try { - momentObj[m.method].apply(momentObj, m.args); - } catch { - console.warn("Invalid moment definition in timeExtent definition. Use current time."); - } - } - }); - } - // Case: Property is a string but not "now" - else if (typeof referenceMoment === 'string' && referenceMoment !== "now") { - momentObj = moment(referenceMoment).utc(); - if (momentObj && !momentObj.isValid()) { - momentObj = moment.utc(); - console.warn("Invalid timeExtent definition. Use current time."); - } - } - - return momentObj.toDate(); - } - - /** - * Generates stops values for the TimeSlider - * - * @returns Object that contains the stop configuration - * @private - */ - _getStops() { - const properties = this._properties; - const stopsProperties = properties.stops; - const defaultStopCount = 10; - let stops = null; - - // If stop properties are defined - if (stopsProperties) { - // Case: Stops are defined using dates - if (stopsProperties.dates) { - stops = {}; - const dates = []; - let momentObj; - stopsProperties.dates.forEach((dateString) => { - momentObj = moment(dateString).utc(); - // Push valid dates - if (momentObj?.isValid()) { - dates.push(momentObj.toDate()); - } - // Warn for invalid dates and skip date - else { - console.warn("Invalid date stop definition. Skip value."); - } - }); - stops.dates = dates; - } - // Check if stops are defined using moments and calculations - else if (stopsProperties.moment) { - stops = {}; - const dates = []; - let momentObj = moment().utc(); - stopsProperties.moment.forEach((timeStop) => { - // Check if leading timeStop element is a string - if (typeof timeStop === 'string') { - momentObj = moment(timeStop).utc(); - if (momentObj?.isValid()) { - dates.push(momentObj.toDate()); - } else { - momentObj = moment.utc(); - console.warn("Invalid stop definition at start of the array. Use current time."); - } - } - // stop is an array - else if (Array.isArray(timeStop)) { - timeStop.forEach((time) => { - try { - momentObj[time.method].apply(momentObj, time.args); - if (momentObj?.isValid()) { - dates.push(momentObj.toDate()); - } - } catch { - console.warn("Invalid stop definition in moment array. Skip array entry."); - } - }); - } - // stop is a single object - else { - try { - momentObj[timeStop.method].apply(momentObj, timeStop.args); - if (momentObj?.isValid()) { - dates.push(momentObj.toDate()); - } - } catch { - console.warn("Invalid stop definition in moment array. Skip array entry."); - } - } - }); - stops.dates = dates; - } - // Case: Stops are defined by count or interval - else { - // Case: Stops defined by count - if (stopsProperties.count) { - stops = {}; - stops.count = stopsProperties.count || defaultStopCount; - } - // Case: Stops defined by interval - else if (stopsProperties.interval) { - stops = {}; - stops.interval = { - value: stopsProperties.interval.value || 1, - unit: stopsProperties.interval.unit || "years" - }; - } - // Case: No definition provided. Use 10 stops instead - else { - stops = {}; - stops.count = defaultStopCount; - } - // Case: Stops defined by start and end time and interval/count - if (stopsProperties.timeExtent && stops) { - let start = null; - let end = null; - if (stopsProperties.timeExtent.start) { - start = moment(stopsProperties.timeExtent.start); - if (!start?.isValid()) { - start = moment.utc(); - console.warn("No valid configuration for stop timeExtent start. Using current time."); - } - } - if (stopsProperties.timeExtent.end) { - end = moment(stopsProperties.timeExtent.end); - if (end && end.isValid() === false) { - end = moment.utc(); - console.warn("No valid configuration for stop timeExtent end. Using current time."); - } - } - if (start && end) { - stops.timeExtent = { - start: start.toDate(), - end: end.toDate() - }; - } - } - } - } - return stops; - } - - _changeAllLayerTimeExtents(timeExtent) { - const mapWidgetModel = this._mapWidgetModel; - const map = mapWidgetModel.map; - const layers = map.layers; - const flattenLayers = this._getFlattenLayers(layers); - flattenLayers.forEach((layer) => { - if (layer.useViewTime) { - if (layer.timeExtent && !layer._initialTimeExtent) { - layer._initialTimeExtent = layer.timeExtent; - } - layer.timeExtent = timeExtent; - } - }); - } - - _resetAllLayerTimeExtents() { - const mapWidgetModel = this._mapWidgetModel; - const map = mapWidgetModel.map; - const layers = map.layers; - const flattenLayers = this._getFlattenLayers(layers); - flattenLayers.forEach((layer) => { - layer.timeExtent = layer._initialTimeExtent; - }); - } - - _getFlattenLayers(layers) { - return layers.flatten(item => item.layers || item.sublayers); - } - - /** - * Converts a date string to a Date Object via moment - * - * @param config Date string - * @returns {Date} - * @private - */ - _getDate(config) { - return moment(config).toDate(); - } - - /** - * Returns the current View of the MapWidgetModel - * - * @returns {Promise} The current View - * @private - */ - _getView() { - const mapWidgetModel = this._mapWidgetModel; - return new Promise((resolve) => { - if (mapWidgetModel.view) { - resolve(mapWidgetModel.view); - } else { - const watcher = mapWidgetModel.watch("view", ({ value: view }) => { - watcher.remove(); - resolve(view); - }); - } - }); - } - - /** - * Resets the initial TimeExtent value of the View - * - * @private - */ - _resetTimeExtent() { - this._getView().then((view) => { - if (this.#initialTimeExtent) { - view.timeExtent = this.#initialTimeExtent; - } - }); - } -} diff --git a/src/main/js/bundles/dn_timeslider/TimeSliderWidgetController.ts b/src/main/js/bundles/dn_timeslider/TimeSliderWidgetController.ts new file mode 100644 index 0000000..217752b --- /dev/null +++ b/src/main/js/bundles/dn_timeslider/TimeSliderWidgetController.ts @@ -0,0 +1,290 @@ +/// +/// Copyright (C) 2023 con terra GmbH (info@conterra.de) +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { InjectedReference } from "apprt-core/InjectedReference"; +import TimeSlider from "esri/widgets/TimeSlider"; +import moment from "moment"; + +import { MapWidgetModel } from "map-widget/api"; +import { TimesliderProperties } from "../../types/TimesliderProperties"; +import TimeExtent from "esri/TimeExtent"; + +export default class TimeSliderWidgetController { + + private _properties: InjectedReference>; + private timeSliderWidget: any = undefined; + private initialTimeExtent: any = undefined; + private labelFormatFunction: any = undefined; + private _mapWidgetModel: InjectedReference; + + public activate(): void { + const properties = this._properties; + this.getView().then((view: __esri.View) => { + if (this._properties.viewTimeExtent) { + view.timeExtent = this.getTimeExtentFromConfig(properties, "viewTimeExtent"); + } + this.initialTimeExtent = view.timeExtent; + }); + } + + public setLabelFormatFunction(labelFormatFunction: __esri.DateLabelFormatter): void { + this.labelFormatFunction = labelFormatFunction; + } + + public deactivate(): void { + this.resetTimeExtent(); + this.destroyWidget(); + } + + public onToolActivated(): void { + this.getView().then((view: __esri.View) => { + view.timeExtent = this.timeSliderWidget.timeExtent; + if (this._properties.playOnStartup) { + this.timeSliderWidget.play(); + } + }); + } + + public onToolDeactivated(): void { + this.timeSliderWidget.stop(); + this.resetTimeExtent(); + } + + public getWidget(properties?: InjectedReference>): TimeSlider { + const timeSliderProperties = this.getTimeSliderProperties(properties || this._properties); + return this.timeSliderWidget = new TimeSlider(timeSliderProperties); + } + + private destroyWidget(): void { + this.timeSliderWidget.destroy(); + this.timeSliderWidget = undefined; + } + + public getTimeSliderProperties(properties: InjectedReference>): TimesliderProperties { + const timeSliderProperties: TimesliderProperties = { + timeExtent: this.getTimeExtentFromConfig(properties, "timeExtent"), + fullTimeExtent: this.getTimeExtentFromConfig(properties, "fullTimeExtent"), + viewTimeExtent: this.getTimeExtentFromConfig(properties, "viewTimeExtent"), + labelFormatFunction: properties?.labelFormatFunction, + stops: properties.stops, + mode: properties.mode, + loop: properties.loop, + playRate: properties.playRate, + playOnStartup: properties.playOnStartup, + timeVisible: properties.timeVisible + }; + const stops = this.getStops(properties); + if (stops) { + timeSliderProperties.stops = stops; + } + + if (this.labelFormatFunction) { + timeSliderProperties.labelFormatFunction = this.labelFormatFunction; + } + + return timeSliderProperties; + } + + private getTimeExtentFromConfig(properties: InjectedReference>, + attribute: string): __esri.TimeExtent { + const timeExtent = properties[attribute]; + if (timeExtent) { + return this.getTimeExtent(timeExtent); + } else { + return undefined; + } + } + + private getTimeExtent(referenceTimeExtent: __esri.TimeExtent): __esri.TimeExtent { + if (moment.isDate(referenceTimeExtent.start) && moment.isDate(referenceTimeExtent.end)) { + return referenceTimeExtent; + } else { + return new TimeExtent({ + start: this.constructDate(referenceTimeExtent.start), + end: this.constructDate(referenceTimeExtent.end) + }); + } + } + + private constructDate(dateRepresentation: string | Date): Date { + if (!dateRepresentation) { + return; + } + + if (dateRepresentation === "now") { + return moment().toDate(); + } + + let momentObj = moment.utc(); + if (Array.isArray(dateRepresentation)) { + dateRepresentation.forEach((date, index) => { + if (typeof date === 'string' && index === 0) { + momentObj = moment(date).utc(); + if (momentObj && !momentObj.isValid()) { + momentObj = moment().utc(); + console.warn("Invalid timeExtent definition. Use current time."); + } + } else { + try { + momentObj[date.method](...date.args); + } catch { + console.warn("Invalid moment definition in timeExtent definition. Use current time."); + } + } + }); + } + else if (typeof dateRepresentation === 'string') { + momentObj = moment(dateRepresentation).utc(); + if (momentObj && !momentObj.isValid()) { + momentObj = moment.utc(); + console.warn("Invalid timeExtent definition. Use current time."); + } + } + + return momentObj.toDate(); + } + + private getStops(properties: InjectedReference>): __esri.StopsByDates | + __esri.StopsByCount | __esri.StopsByInterval | undefined { + const stopsProperties = properties.stops; + const defaultStopCount = 10; + let stops = null; + + if (!stopsProperties) { + return; + } + + if (stopsProperties && !Object.keys(stopsProperties).length){ + stops = {}; + stops.count = defaultStopCount; + } + + if (stopsProperties.dates) { + stops = {}; + const dates = []; + let momentObj: moment; + stopsProperties.dates.forEach((dateString: string) => { + momentObj = moment(dateString).utc(); + if (momentObj?.isValid()) { + dates.push(momentObj.toDate()); + } + else { + console.warn("Invalid date stop definition. Skip value."); + } + }); + stops.dates = dates; + } + else if (stopsProperties.count) { + stops = {}; + stops.count = stopsProperties.count || defaultStopCount; + } + else if (stopsProperties.interval) { + stops = {}; + stops.interval = { + value: stopsProperties.interval.value || 1, + unit: stopsProperties.interval.unit || "years" + }; + } + else if (stopsProperties.moment) { + stops = {}; + const dates = []; + let momentObj = moment().utc(); + + stopsProperties.moment.forEach((timeStop: string | Array | moment) => { + if (typeof timeStop === 'string') { + momentObj = moment(timeStop).utc(); + if (momentObj?.isValid()) { + dates.push(momentObj.toDate()); + } else { + momentObj = moment.utc(); + console.warn("Invalid stop definition at start of the array. Use current time."); + } + } + else if (Array.isArray(timeStop)) { + timeStop.forEach((time) => { + try { + momentObj[time.method](...time.args); + if (momentObj?.isValid()) { + dates.push(momentObj.toDate()); + } + } catch { + console.warn("Invalid stop definition in moment array. Skip array entry."); + } + }); + } + else { + try { + momentObj[timeStop.method](...timeStop.args); + if (momentObj?.isValid()) { + dates.push(momentObj.toDate()); + } + } catch { + console.warn("Invalid stop definition in moment array. Skip array entry."); + } + } + }); + stops.dates = dates; + } + else { + if (stopsProperties.timeExtent && stops) { + let start = null; + let end = null; + if (stopsProperties.timeExtent.start) { + start = moment(stopsProperties.timeExtent.start); + if (!start?.isValid()) { + start = moment.utc(); + console.warn("No valid configuration for stop timeExtent start. Using current time."); + } + } + if (stopsProperties.timeExtent.end) { + end = moment(stopsProperties.timeExtent.end); + if (end && end.isValid() === false) { + end = moment.utc(); + console.warn("No valid configuration for stop timeExtent end. Using current time."); + } + } + if (start && end) { + stops.timeExtent = { + start: start.toDate(), + end: end.toDate() + }; + } + } + } + + return stops; + } + + private getView(): Promise<__esri.View> { + const mapWidgetModel = this._mapWidgetModel; + return new Promise((resolve) => { + if (mapWidgetModel.view) { + resolve(mapWidgetModel.view); + } else { + const watcher = mapWidgetModel.watch("view", ({value: view}) => { + watcher.remove(); + resolve(view); + }); + } + }); + } + + private resetTimeExtent(): void { + this.getView().then((view: __esri.View) => { + view.timeExtent = this.initialTimeExtent; + }); + } +} diff --git a/src/main/js/bundles/dn_timeslider/TimeSliderWidgetFactory.js b/src/main/js/bundles/dn_timeslider/TimeSliderWidgetFactory.js deleted file mode 100644 index 232477a..0000000 --- a/src/main/js/bundles/dn_timeslider/TimeSliderWidgetFactory.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2023 con terra GmbH (info@conterra.de) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import EsriDijit from "esri-widgets/EsriDijit"; -import Binding from "apprt-binding/Binding"; - -const _binding = Symbol("_binding"); - -export default class TimeSliderWidgetFactory { - - deactivate() { - this._deactivateBinding(); - } - - createInstance() { - return this._getWidget(); - } - - _getWidget() { - const timeSliderWidget = this._timeSliderWidgetController.getWidget(); - const mapWidgetModel = this._mapWidgetModel; - const binding = this[_binding] = Binding.for(timeSliderWidget, mapWidgetModel) - .syncToLeft("view") - .enable() - .syncToLeftNow(); - - timeSliderWidget.own(binding); - - return new EsriDijit(timeSliderWidget); - } - - _deactivateBinding() { - this[_binding].unbind(); - this[_binding] = undefined; - } -} diff --git a/src/main/js/bundles/dn_timeslider/TimeSliderWidgetFactory.ts b/src/main/js/bundles/dn_timeslider/TimeSliderWidgetFactory.ts new file mode 100644 index 0000000..8854efe --- /dev/null +++ b/src/main/js/bundles/dn_timeslider/TimeSliderWidgetFactory.ts @@ -0,0 +1,54 @@ +/// +/// Copyright (C) 2023 con terra GmbH (info@conterra.de) +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { InjectedReference } from "apprt-core/InjectedReference"; +import EsriDijit from "esri-widgets/EsriDijit"; +import Binding, { Bindable, Binding as BindingType } from 'apprt-binding/Binding'; + +import { MapWidgetModel } from "map-widget/api"; +import type TimeSliderWidgetController from "./TimeSliderWidgetController"; + +export default class TimeSliderWidgetFactory { + + private binding: BindingType; + private _mapWidgetModel: InjectedReference; + private _timeSliderWidgetController: TimeSliderWidgetController; + + public deactivate(): void { + this.deactivateBinding(); + } + + public createInstance(): any { + return this.getWidget(); + } + + private getWidget(): any { + const timeSliderWidget = this._timeSliderWidgetController.getWidget(); + const mapWidgetModel = this._mapWidgetModel; + + this.binding = Binding.for(timeSliderWidget as Bindable, mapWidgetModel) + .syncToLeft("view") + .enable() + .syncToLeftNow(); + + return new (EsriDijit as any)(timeSliderWidget); + } + + private deactivateBinding(): void { + this.binding.unbind(); + this.binding = undefined; + } +} diff --git a/src/main/js/bundles/dn_timeslider/main.js b/src/main/js/bundles/dn_timeslider/main.js deleted file mode 100644 index 44be8f2..0000000 --- a/src/main/js/bundles/dn_timeslider/main.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright (C) 2023 con terra GmbH (info@conterra.de) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import "dojo/i18n!./nls/bundle"; diff --git a/src/main/js/bundles/dn_timeslider/main.ts b/src/main/js/bundles/dn_timeslider/main.ts new file mode 100644 index 0000000..159bfb9 --- /dev/null +++ b/src/main/js/bundles/dn_timeslider/main.ts @@ -0,0 +1,17 @@ +/// +/// Copyright (C) 2023 con terra GmbH (info@conterra.de) +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import "dojo/i18n!./nls/bundle"; diff --git a/src/main/js/bundles/dn_timeslider/manifest.json b/src/main/js/bundles/dn_timeslider/manifest.json index 5396a23..9d06dd1 100644 --- a/src/main/js/bundles/dn_timeslider/manifest.json +++ b/src/main/js/bundles/dn_timeslider/manifest.json @@ -82,6 +82,62 @@ "l": 50 } } + }, + { + "widgetRole": "layerTimeSliderWidget", + "window": { + "title": "${windowTitle}", + "closable": true, + "maximizable": true, + "minimizeOnClose": false, + "resizable": true, + "marginBox": { + "w": 435, + "h": 230, + "l": 20, + "t": 125 + }, + "minSize": { + "w": 435, + "h": 230 + }, + "windowClass": "timeslider-window" + } + }, + { + "widgetRole": "layerTimeSliderWidget", + "sublayout": [ + "mobile_landscape", + "mobile_portrait" + ], + "window": { + "resizable": false, + "fixEdgesInViewport": { + "l": true, + "r": true + }, + "marginBox": { + "h": 230, + "b": 42, + "r": 0, + "l": 0 + } + } + }, + { + "widgetRole": "layerTimeSliderWidget", + "sublayout": [ + "mobile_landscape" + ], + "window": { + "resizable": false, + "marginBox": { + "h": 230, + "b": 0, + "r": 0, + "l": 50 + } + } } ], "components": [ @@ -128,7 +184,7 @@ { "name": "_mapWidgetModel", "providing": "map-widget.MapWidgetModel" - }, +}, { "name": "labelFormatFunction", "providing": "dn_timeslider.LabelFormatFunction", @@ -160,6 +216,16 @@ "providing": "dn_timeslider.TimeSliderWidgetController" } ] + }, + { + "name": "TimeSliderTocActionDefinitionFactory", + "provides": "toc.ActionDefinitionFactory", + "references": [ + { + "name": "_timeSliderWidgetController", + "providing": "dn_timeslider.TimeSliderWidgetController" + } + ] } ] } diff --git a/src/main/js/bundles/dn_timeslider/module.js b/src/main/js/bundles/dn_timeslider/module.js deleted file mode 100644 index b03456f..0000000 --- a/src/main/js/bundles/dn_timeslider/module.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (C) 2023 con terra GmbH (info@conterra.de) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import "."; -import "./TimeSliderWidgetFactory"; -import "./TimeSliderWidgetController"; -import "ct/tools/Tool"; diff --git a/src/main/js/bundles/dn_timeslider/module.ts b/src/main/js/bundles/dn_timeslider/module.ts new file mode 100644 index 0000000..5765c92 --- /dev/null +++ b/src/main/js/bundles/dn_timeslider/module.ts @@ -0,0 +1,21 @@ +/// +/// Copyright (C) 2023 con terra GmbH (info@conterra.de) +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import "."; +import "./TimeSliderWidgetFactory"; +import "./TimeSliderWidgetController"; +import "./TimeSliderTocActionDefinitionFactory"; +import "ct/tools/Tool"; diff --git a/src/main/js/bundles/dn_timeslider/nls/bundle.js b/src/main/js/bundles/dn_timeslider/nls/bundle.js deleted file mode 100644 index e501e1e..0000000 --- a/src/main/js/bundles/dn_timeslider/nls/bundle.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2023 con terra GmbH (info@conterra.de) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -module.exports = { - root: { - bundleName: "Time Slider", - bundleDescription: "The Time Slider bundle allows the user to change the time extent of the map.", - windowTitle: "Time Slider", - tool: { - title: "Time Slider", - tooltip: "Time Slider" - } - }, - de: true -}; diff --git a/src/main/js/bundles/dn_timeslider/nls/bundle.ts b/src/main/js/bundles/dn_timeslider/nls/bundle.ts new file mode 100644 index 0000000..51fd0b0 --- /dev/null +++ b/src/main/js/bundles/dn_timeslider/nls/bundle.ts @@ -0,0 +1,35 @@ +/// +/// Copyright (C) 2023 con terra GmbH (info@conterra.de) +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +const i18n = { + root: { + bundleName: "Time Slider", + bundleDescription: "The Time Slider bundle allows the user to change the time extent of the map.", + windowTitle: "Time Slider", + tool: { + title: "Time Slider", + tooltip: "Time Slider" + }, + tocActionLabel: "Time Slider for this Layer" + }, + de: true +}; + +export type Messages = (typeof i18n)["root"]; +export interface MessagesReference { + get: () => Messages +} +export default i18n; diff --git a/src/main/js/bundles/dn_timeslider/nls/de/bundle.js b/src/main/js/bundles/dn_timeslider/nls/de/bundle.js deleted file mode 100644 index cad2724..0000000 --- a/src/main/js/bundles/dn_timeslider/nls/de/bundle.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2023 con terra GmbH (info@conterra.de) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -module.exports = { - bundleName: "Time Slider", - bundleDescription: "Das Time Slider Bundle erlaubt es dem Anwender den Zeitrahmen der Karte anzupassen.", - windowTitle: "Time Slider", - tool: { - title: "Time Slider", - tooltip: "Time Slider" - } -}; diff --git a/src/main/js/bundles/dn_timeslider/nls/de/bundle.ts b/src/main/js/bundles/dn_timeslider/nls/de/bundle.ts new file mode 100644 index 0000000..578121e --- /dev/null +++ b/src/main/js/bundles/dn_timeslider/nls/de/bundle.ts @@ -0,0 +1,28 @@ +/// +/// Copyright (C) 2023 con terra GmbH (info@conterra.de) +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Messages } from "../bundle"; + +export default { + bundleName: "Time Slider", + bundleDescription: "Das Time Slider Bundle erlaubt es dem Anwender den Zeitrahmen der Karte anzupassen.", + windowTitle: "Time Slider", + tool: { + title: "Time Slider", + tooltip: "Time Slider" + }, + tocActionLabel: "Time Slider für diesen Layer" +} satisfies Messages; diff --git a/src/main/js/bundles/dn_timeslider/tests/all.js b/src/main/js/bundles/dn_timeslider/tests/all.js deleted file mode 100644 index e668b93..0000000 --- a/src/main/js/bundles/dn_timeslider/tests/all.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (C) 2023 con terra GmbH (info@conterra.de) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import "./TimeSliderWidgetController.spec"; diff --git a/src/main/js/bundles/dn_timeslider/tests/all.ts b/src/main/js/bundles/dn_timeslider/tests/all.ts new file mode 100644 index 0000000..b51bd00 --- /dev/null +++ b/src/main/js/bundles/dn_timeslider/tests/all.ts @@ -0,0 +1,17 @@ +/// +/// Copyright (C) 2023 con terra GmbH (info@conterra.de) +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import "./TimeSliderWidgetController.spec"; diff --git a/src/main/js/types/ExtendedLayer.d.ts b/src/main/js/types/ExtendedLayer.d.ts new file mode 100644 index 0000000..600c57e --- /dev/null +++ b/src/main/js/types/ExtendedLayer.d.ts @@ -0,0 +1,20 @@ +/// +/// Copyright (C) 2023 con terra GmbH (info@conterra.de) +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export interface ExtendedLayer extends __esri.Layer { + timeInfo: __esri.TimeInfo, + stops: __esri.StopsByDates | __esri.StopsByCount | __esri.StopsByInterval +} diff --git a/src/main/js/types/TimesliderProperties.d.ts b/src/main/js/types/TimesliderProperties.d.ts new file mode 100644 index 0000000..5367a89 --- /dev/null +++ b/src/main/js/types/TimesliderProperties.d.ts @@ -0,0 +1,28 @@ +/// +/// Copyright (C) 2023 con terra GmbH (info@conterra.de) +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +export interface TimesliderProperties { + timeExtent: __esri.TimeExtent, + fullTimeExtent: __esri.TimeExtent, + viewTimeExtent: __esri.TimeExtent, + labelFormatFunction: __esri.DateLabelFormatter, + stops: __esri.StopsByDates | __esri.StopsByCount | __esri.StopsByInterval + mode: "instant" | "time-window" | "cumulative-from-start" | "cumulative-from-end", + loop: boolean, + playRate: number, + playOnStartup: boolean, + timeVisible: boolean +}