@jupytergis/base 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/annotations/components/Annotation.js +2 -2
- package/lib/annotations/model.d.ts +6 -7
- package/lib/annotations/model.js +15 -15
- package/lib/commands.d.ts +2 -3
- package/lib/commands.js +146 -62
- package/lib/constants.d.ts +3 -0
- package/lib/constants.js +5 -1
- package/lib/dialogs/formdialog.d.ts +5 -0
- package/lib/dialogs/formdialog.js +2 -2
- package/lib/dialogs/layerBrowserDialog.d.ts +4 -5
- package/lib/dialogs/layerBrowserDialog.js +9 -9
- package/lib/dialogs/symbology/components/color_ramp/ModeSelectRow.js +2 -1
- package/lib/dialogs/symbology/hooks/useGetBandInfo.d.ts +26 -0
- package/lib/dialogs/symbology/hooks/useGetBandInfo.js +64 -0
- package/lib/dialogs/symbology/hooks/useGetProperties.d.ts +1 -1
- package/lib/dialogs/symbology/hooks/useGetProperties.js +12 -9
- package/lib/dialogs/symbology/symbologyDialog.d.ts +2 -3
- package/lib/dialogs/symbology/symbologyDialog.js +10 -9
- package/lib/dialogs/symbology/tiff_layer/TiffRendering.d.ts +1 -1
- package/lib/dialogs/symbology/tiff_layer/TiffRendering.js +16 -3
- package/lib/dialogs/symbology/tiff_layer/components/BandRow.d.ts +16 -3
- package/lib/dialogs/symbology/tiff_layer/components/BandRow.js +21 -7
- package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.d.ts +4 -0
- package/lib/dialogs/symbology/tiff_layer/types/MultibandColor.js +85 -0
- package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.d.ts +1 -20
- package/lib/dialogs/symbology/tiff_layer/types/SingleBandPseudoColor.js +25 -65
- package/lib/dialogs/symbology/vector_layer/VectorRendering.d.ts +1 -1
- package/lib/dialogs/symbology/vector_layer/VectorRendering.js +18 -13
- package/lib/dialogs/symbology/vector_layer/types/Categorized.d.ts +1 -1
- package/lib/dialogs/symbology/vector_layer/types/Categorized.js +30 -19
- package/lib/dialogs/symbology/vector_layer/types/Graduated.d.ts +1 -1
- package/lib/dialogs/symbology/vector_layer/types/Graduated.js +16 -13
- package/lib/dialogs/symbology/vector_layer/types/Heatmap.d.ts +4 -0
- package/lib/dialogs/symbology/vector_layer/types/Heatmap.js +77 -0
- package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.d.ts +1 -1
- package/lib/dialogs/symbology/vector_layer/types/SimpleSymbol.js +4 -3
- package/lib/formbuilder/creationform.d.ts +6 -2
- package/lib/formbuilder/creationform.js +6 -6
- package/lib/formbuilder/editform.d.ts +2 -2
- package/lib/formbuilder/editform.js +14 -9
- package/lib/formbuilder/formselectors.js +11 -1
- package/lib/formbuilder/objectform/baseform.d.ts +12 -3
- package/lib/formbuilder/objectform/baseform.js +39 -0
- package/lib/formbuilder/objectform/fileselectorwidget.d.ts +2 -0
- package/lib/formbuilder/objectform/fileselectorwidget.js +88 -0
- package/lib/formbuilder/objectform/geojsonsource.d.ts +5 -7
- package/lib/formbuilder/objectform/geojsonsource.js +8 -24
- package/lib/formbuilder/objectform/geotiffsource.d.ts +5 -1
- package/lib/formbuilder/objectform/geotiffsource.js +64 -18
- package/lib/formbuilder/objectform/heatmapLayerForm.d.ts +11 -0
- package/lib/formbuilder/objectform/heatmapLayerForm.js +60 -0
- package/lib/formbuilder/objectform/layerform.d.ts +2 -0
- package/lib/formbuilder/objectform/layerform.js +6 -0
- package/lib/formbuilder/objectform/pathbasedsource.d.ts +19 -0
- package/lib/formbuilder/objectform/pathbasedsource.js +98 -0
- package/lib/formbuilder/objectform/vectorlayerform.d.ts +0 -2
- package/lib/formbuilder/objectform/vectorlayerform.js +0 -59
- package/lib/icons.d.ts +1 -0
- package/lib/icons.js +5 -0
- package/lib/keybindings.json +62 -0
- package/lib/mainview/TemporalSlider.d.ts +8 -0
- package/lib/mainview/TemporalSlider.js +303 -0
- package/lib/mainview/mainView.d.ts +46 -8
- package/lib/mainview/mainView.js +431 -144
- package/lib/mainview/mainviewmodel.d.ts +4 -0
- package/lib/mainview/mainviewmodel.js +4 -0
- package/lib/mainview/mainviewwidget.d.ts +0 -2
- package/lib/mainview/mainviewwidget.js +0 -2
- package/lib/panelview/annotationPanel.js +5 -5
- package/lib/panelview/components/filter-panel/Filter.js +8 -24
- package/lib/panelview/components/identify-panel/IdentifyPanel.js +1 -1
- package/lib/panelview/components/layers.js +2 -2
- package/lib/panelview/components/sources.js +1 -1
- package/lib/panelview/leftpanel.d.ts +3 -0
- package/lib/panelview/leftpanel.js +5 -1
- package/lib/panelview/model.js +8 -8
- package/lib/panelview/objectproperties.js +10 -10
- package/lib/panelview/rightpanel.d.ts +1 -1
- package/lib/panelview/rightpanel.js +10 -10
- package/lib/statusbar/StatusBar.d.ts +13 -0
- package/lib/statusbar/StatusBar.js +52 -0
- package/lib/toolbar/widget.d.ts +1 -1
- package/lib/toolbar/widget.js +44 -37
- package/lib/tools.d.ts +50 -7
- package/lib/tools.js +394 -12
- package/lib/types.d.ts +2 -0
- package/lib/widget.d.ts +29 -5
- package/lib/widget.js +41 -7
- package/package.json +17 -5
- package/style/base.css +11 -0
- package/style/icons/logo_mini_qgz.svg +31 -0
- package/style/leftPanel.css +8 -0
- package/style/statusBar.css +16 -0
- package/style/symbologyDialog.css +7 -1
- package/style/temporalSlider.css +47 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { showErrorMessage } from '@jupyterlab/apputils';
|
|
2
|
+
import { BaseForm } from './baseform';
|
|
3
|
+
import { loadFile } from '../../tools';
|
|
4
|
+
import { FileSelectorWidget } from './fileselectorwidget';
|
|
5
|
+
/**
|
|
6
|
+
* The form to modify a PathBasedSource source.
|
|
7
|
+
*/
|
|
8
|
+
export class PathBasedSourcePropertiesForm extends BaseForm {
|
|
9
|
+
constructor(props) {
|
|
10
|
+
var _a, _b;
|
|
11
|
+
super(props);
|
|
12
|
+
if (this.props.sourceType !== 'GeoJSONSource') {
|
|
13
|
+
this._validatePath((_b = (_a = props.sourceData) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : '');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
processSchema(data, schema, uiSchema) {
|
|
17
|
+
var _a;
|
|
18
|
+
super.processSchema(data, schema, uiSchema);
|
|
19
|
+
if (!schema.properties || !data) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Customize the widget for path field
|
|
23
|
+
if (schema.properties && schema.properties.path) {
|
|
24
|
+
const docManager = (_a = this.props.formChangedSignal) === null || _a === void 0 ? void 0 : _a.sender.props.formSchemaRegistry.getDocManager();
|
|
25
|
+
uiSchema.path = {
|
|
26
|
+
'ui:widget': FileSelectorWidget,
|
|
27
|
+
'ui:options': {
|
|
28
|
+
docManager,
|
|
29
|
+
formOptions: this.props
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// This is not user-editable
|
|
34
|
+
delete schema.properties.valid;
|
|
35
|
+
}
|
|
36
|
+
onFormBlur(id, value) {
|
|
37
|
+
// Is there a better way to spot the path text entry?
|
|
38
|
+
if (!id.endsWith('_path')) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this._validatePath(value);
|
|
42
|
+
}
|
|
43
|
+
// we need to use `onFormChange` instead of `onFormBlur` because it's no longer a text field
|
|
44
|
+
onFormChange(e) {
|
|
45
|
+
var _a;
|
|
46
|
+
super.onFormChange(e);
|
|
47
|
+
if (((_a = e.formData) === null || _a === void 0 ? void 0 : _a.path) !== undefined) {
|
|
48
|
+
this._validatePath(e.formData.path);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
onFormSubmit(e) {
|
|
52
|
+
var _a, _b, _c;
|
|
53
|
+
if (((_c = (_b = (_a = this.state.extraErrors) === null || _a === void 0 ? void 0 : _a.path) === null || _b === void 0 ? void 0 : _b.__errors) === null || _c === void 0 ? void 0 : _c.length) >= 1) {
|
|
54
|
+
showErrorMessage('Invalid file', this.state.extraErrors.path.__errors[0]);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
super.onFormSubmit(e);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Validate the path, to avoid invalid path.
|
|
61
|
+
*
|
|
62
|
+
* @param path - the path to validate.
|
|
63
|
+
*/
|
|
64
|
+
async _validatePath(path) {
|
|
65
|
+
const extraErrors = this.state.extraErrors;
|
|
66
|
+
let error = '';
|
|
67
|
+
let valid = true;
|
|
68
|
+
if (!path) {
|
|
69
|
+
valid = false;
|
|
70
|
+
error = 'Path is required';
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
try {
|
|
74
|
+
await loadFile({
|
|
75
|
+
filepath: path,
|
|
76
|
+
type: this.props.sourceType,
|
|
77
|
+
model: this.props.model
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
valid = false;
|
|
82
|
+
error = `"${path}" is not a valid ${this.props.sourceType} file.`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!valid) {
|
|
86
|
+
extraErrors.path = {
|
|
87
|
+
__errors: [error]
|
|
88
|
+
};
|
|
89
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { extraErrors })));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
this.setState(old => (Object.assign(Object.assign({}, old), { extraErrors: Object.assign(Object.assign({}, extraErrors), { path: { __errors: [] } }) })));
|
|
93
|
+
}
|
|
94
|
+
if (this.props.formErrorSignal) {
|
|
95
|
+
this.props.formErrorSignal.emit(!valid);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -6,10 +6,8 @@ import { ILayerProps, LayerPropertiesForm } from './layerform';
|
|
|
6
6
|
*/
|
|
7
7
|
export declare class VectorLayerPropertiesForm extends LayerPropertiesForm {
|
|
8
8
|
protected currentFormData: IVectorLayer;
|
|
9
|
-
private sourceLayers;
|
|
10
9
|
private currentSourceId;
|
|
11
10
|
constructor(props: ILayerProps);
|
|
12
11
|
protected onFormChange(e: IChangeEvent): void;
|
|
13
12
|
protected processSchema(data: IVectorLayer | undefined, schema: IDict, uiSchema: IDict): void;
|
|
14
|
-
private fetchSourceLayers;
|
|
15
13
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getSourceLayerNames } from '../../tools';
|
|
2
1
|
import { LayerPropertiesForm } from './layerform';
|
|
3
2
|
/**
|
|
4
3
|
* The form to modify a vector layer.
|
|
@@ -6,29 +5,6 @@ import { LayerPropertiesForm } from './layerform';
|
|
|
6
5
|
export class VectorLayerPropertiesForm extends LayerPropertiesForm {
|
|
7
6
|
constructor(props) {
|
|
8
7
|
super(props);
|
|
9
|
-
this.sourceLayers = [];
|
|
10
|
-
this.fetchSourceLayers(this.props.sourceData);
|
|
11
|
-
// If there is a source form attached, we listen to its changes
|
|
12
|
-
if (this.sourceFormChangedSignal) {
|
|
13
|
-
this.sourceFormChangedSignal.connect((sender, sourceData) => {
|
|
14
|
-
if (this.props.sourceType === 'VectorTileSource') {
|
|
15
|
-
this.fetchSourceLayers(this.currentFormData, sourceData);
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
props.model.clientStateChanged.connect(() => {
|
|
20
|
-
var _a;
|
|
21
|
-
if (!((_a = props.model.localState) === null || _a === void 0 ? void 0 : _a.selected.value)) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
const l = this.props.model.getLayer(Object.keys(props.model.localState.selected.value)[0]);
|
|
25
|
-
const source = this.props.model.getSource(l === null || l === void 0 ? void 0 : l.parameters.source);
|
|
26
|
-
if (!source || source.type !== 'VectorTileSource') {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const sourceData = source.parameters;
|
|
30
|
-
this.fetchSourceLayers(this.currentFormData, sourceData);
|
|
31
|
-
});
|
|
32
8
|
}
|
|
33
9
|
onFormChange(e) {
|
|
34
10
|
super.onFormChange(e);
|
|
@@ -40,7 +16,6 @@ export class VectorLayerPropertiesForm extends LayerPropertiesForm {
|
|
|
40
16
|
if (!source || source.type !== 'VectorTileSource') {
|
|
41
17
|
return;
|
|
42
18
|
}
|
|
43
|
-
this.fetchSourceLayers(this.currentFormData, source.parameters);
|
|
44
19
|
}
|
|
45
20
|
processSchema(data, schema, uiSchema) {
|
|
46
21
|
this.removeFormEntry('color', data, schema, uiSchema);
|
|
@@ -49,39 +24,5 @@ export class VectorLayerPropertiesForm extends LayerPropertiesForm {
|
|
|
49
24
|
if (!data) {
|
|
50
25
|
return;
|
|
51
26
|
}
|
|
52
|
-
// Show a dropdown for available sourceLayers if available
|
|
53
|
-
// And automatically select one
|
|
54
|
-
if (this.sourceLayers.length !== 0) {
|
|
55
|
-
if (!data.sourceLayer || !this.sourceLayers.includes(data.sourceLayer)) {
|
|
56
|
-
data.sourceLayer = this.sourceLayers[0];
|
|
57
|
-
}
|
|
58
|
-
schema.properties.sourceLayer.enum = this.sourceLayers;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
async fetchSourceLayers(data, sourceData) {
|
|
62
|
-
if (data && data.source) {
|
|
63
|
-
this.currentSourceId = data.source;
|
|
64
|
-
if (!sourceData) {
|
|
65
|
-
const currentSource = this.props.model.getSource(data.source);
|
|
66
|
-
if (!currentSource || currentSource.type !== 'VectorTileSource') {
|
|
67
|
-
this.sourceLayers = [];
|
|
68
|
-
this.forceUpdate();
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
sourceData = currentSource.parameters;
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
this.sourceLayers = await getSourceLayerNames(sourceData.url, sourceData.urlParameters);
|
|
75
|
-
this.forceUpdate();
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
console.error(e);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
this.currentSourceId = '';
|
|
83
|
-
this.sourceLayers = [];
|
|
84
|
-
this.forceUpdate();
|
|
85
|
-
}
|
|
86
27
|
}
|
|
87
28
|
}
|
package/lib/icons.d.ts
CHANGED
package/lib/icons.js
CHANGED
|
@@ -12,6 +12,7 @@ import visibilitySvgStr from '../style/icons/visibility.svg';
|
|
|
12
12
|
import nonVisibilitySvgStr from '../style/icons/nonvisibility.svg';
|
|
13
13
|
import geoJsonSvgStr from '../style/icons/geojson.svg';
|
|
14
14
|
import moundSvgStr from '../style/icons/mound.svg';
|
|
15
|
+
import logoMiniQGZ from '../style/icons/logo_mini_qgz.svg';
|
|
15
16
|
export const logoIcon = new LabIcon({
|
|
16
17
|
name: 'jupytergis::logo',
|
|
17
18
|
svgstr: logoSvgStr
|
|
@@ -44,3 +45,7 @@ export const moundIcon = new LabIcon({
|
|
|
44
45
|
name: 'jupytergis::mound',
|
|
45
46
|
svgstr: moundSvgStr
|
|
46
47
|
});
|
|
48
|
+
export const logoMiniIconQGZ = new LabIcon({
|
|
49
|
+
name: 'jupytergis::logoQGZ',
|
|
50
|
+
svgstr: logoMiniQGZ
|
|
51
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"command": "jupytergis:undo",
|
|
4
|
+
"keys": ["Accel Z"],
|
|
5
|
+
"selector": ".data-jgis-keybinding"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"command": "jupytergis:redo",
|
|
9
|
+
"keys": ["Accel Shift Z"],
|
|
10
|
+
"selector": ".data-jgis-keybinding"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"command": "jupytergis:identify",
|
|
14
|
+
"keys": ["Escape"],
|
|
15
|
+
"selector": ".data-jgis-keybinding"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"command": "jupytergis:removeSource",
|
|
19
|
+
"keys": ["Delete"],
|
|
20
|
+
"selector": ".data-jgis-keybinding .jp-gis-source.jp-gis-sourceUnused"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"command": "jupytergis:renameSource",
|
|
24
|
+
"keys": ["F2"],
|
|
25
|
+
"selector": ".data-jgis-keybinding .jp-gis-source"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"command": "jupytergis:removeLayer",
|
|
29
|
+
"keys": ["Delete"],
|
|
30
|
+
"selector": ".data-jgis-keybinding .jp-gis-layerItem"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"command": "jupytergis:renameLayer",
|
|
34
|
+
"keys": ["F2"],
|
|
35
|
+
"selector": ".data-jgis-keybinding .jp-gis-layerItem"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"command": "jupytergis:removeGroup",
|
|
39
|
+
"keys": ["Delete"],
|
|
40
|
+
"selector": ".data-jgis-keybinding .jp-gis-layerGroupHeader"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"command": "jupytergis:renameGroup",
|
|
44
|
+
"keys": ["F2"],
|
|
45
|
+
"selector": ".data-jgis-keybinding .jp-gis-layerGroupHeader"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"command": "jupytergis:executeConsole",
|
|
49
|
+
"keys": ["Shift Enter"],
|
|
50
|
+
"selector": ".jpgis-console .jp-CodeConsole[data-jp-interaction-mode='notebook'] .jp-CodeConsole-promptCell"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"command": "jupytergis:invokeConsoleCompleter",
|
|
54
|
+
"keys": ["Tab"],
|
|
55
|
+
"selector": ".jpgis-console .jp-CodeConsole-promptCell .jp-mod-completer-enabled"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"command": "jupytergis:selectConsoleCompleter",
|
|
59
|
+
"keys": ["Enter"],
|
|
60
|
+
"selector": ".jpgis-console .jp-ConsolePanel .jp-mod-completer-active"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { IDict, IJGISFilterItem, IJupyterGISModel } from '@jupytergis/schema';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
interface ITemporalSliderProps {
|
|
4
|
+
model: IJupyterGISModel;
|
|
5
|
+
filterStates: IDict<IJGISFilterItem | undefined>;
|
|
6
|
+
}
|
|
7
|
+
declare const TemporalSlider: ({ model, filterStates }: ITemporalSliderProps) => React.JSX.Element;
|
|
8
|
+
export default TemporalSlider;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { faPause, faPlay } from '@fortawesome/free-solid-svg-icons';
|
|
2
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
3
|
+
import { Button, Slider } from '@jupyter/react-components';
|
|
4
|
+
import { format, isValid, parse } from 'date-fns';
|
|
5
|
+
import { daysInYear, millisecondsInDay, millisecondsInHour, millisecondsInMinute, millisecondsInSecond, millisecondsInWeek, minutesInMonth } from 'date-fns/constants';
|
|
6
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
7
|
+
import { useGetProperties } from '../dialogs/symbology/hooks/useGetProperties';
|
|
8
|
+
// List of common date formats to try
|
|
9
|
+
// TODO: Not even close to every valid format
|
|
10
|
+
const commonDateFormats = [
|
|
11
|
+
'yyyy-MM-dd', // ISO format (e.g., 2023-10-05)
|
|
12
|
+
'dd/MM/yyyy', // European format (e.g., 05/10/2023)
|
|
13
|
+
'MM/dd/yyyy', // US format (e.g., 10/05/2023)
|
|
14
|
+
'yyyyMMdd', // Compact format (e.g., 20231005)
|
|
15
|
+
'dd-MM-yyyy', // European format with hyphens (e.g., 05-10-2023)
|
|
16
|
+
'MM-dd-yyyy', // US format with hyphens (e.g., 10-05-2023)
|
|
17
|
+
'yyyy/MM/dd', // ISO format with slashes (e.g., 2023/10/05)
|
|
18
|
+
'dd.MM.yyyy', // European format with dots (e.g., 05.10.2023)
|
|
19
|
+
'MM.dd.yyyy' // US format with dots (e.g., 10.05.2023)
|
|
20
|
+
];
|
|
21
|
+
// Time steps in milliseconds
|
|
22
|
+
const stepMap = {
|
|
23
|
+
millisecond: 1,
|
|
24
|
+
second: millisecondsInSecond,
|
|
25
|
+
minute: millisecondsInMinute,
|
|
26
|
+
hour: millisecondsInHour,
|
|
27
|
+
day: millisecondsInDay,
|
|
28
|
+
week: millisecondsInWeek,
|
|
29
|
+
month: minutesInMonth * millisecondsInMinute,
|
|
30
|
+
year: millisecondsInDay * daysInYear
|
|
31
|
+
};
|
|
32
|
+
const TemporalSlider = ({ model, filterStates }) => {
|
|
33
|
+
const [layerId, setLayerId] = useState('');
|
|
34
|
+
const [selectedFeature, setSelectedFeature] = useState('');
|
|
35
|
+
const [range, setRange] = useState({ start: 0, end: 1 }); // min/max of current range being displayed
|
|
36
|
+
const [minMax, setMinMax] = useState({ min: 0, max: 1 }); // min/max of data values
|
|
37
|
+
const [validFeatures, setValidFeatures] = useState([]);
|
|
38
|
+
const [dateFormat, setDateFormat] = useState('yyyy-MM-dd');
|
|
39
|
+
const [step, setStep] = useState(stepMap.year);
|
|
40
|
+
const [currentValue, setCurrentValue] = useState(0);
|
|
41
|
+
const [fps, setFps] = useState(1);
|
|
42
|
+
const [validSteps, setValidSteps] = useState({});
|
|
43
|
+
const layerIdRef = useRef('');
|
|
44
|
+
const intervalRef = useRef(null);
|
|
45
|
+
const { featureProperties } = useGetProperties({ layerId, model });
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
// This is for when the selected layer changes
|
|
48
|
+
const handleClientStateChanged = () => {
|
|
49
|
+
var _a, _b;
|
|
50
|
+
if (!((_b = (_a = model.localState) === null || _a === void 0 ? void 0 : _a.selected) === null || _b === void 0 ? void 0 : _b.value)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const selectedLayerId = Object.keys(model.localState.selected.value)[0];
|
|
54
|
+
// reset
|
|
55
|
+
if (selectedLayerId !== layerIdRef.current) {
|
|
56
|
+
setLayerId(selectedLayerId);
|
|
57
|
+
setDateFormat('yyyy-MM-dd');
|
|
58
|
+
setFps(1);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
// this is for when the layer itself changes
|
|
62
|
+
const handleLayerChange = (_, change) => {
|
|
63
|
+
var _a;
|
|
64
|
+
// Get the changes for the selected layer
|
|
65
|
+
const selectedLayer = (_a = change.layerChange) === null || _a === void 0 ? void 0 : _a.find(layer => layer.id === layerIdRef.current);
|
|
66
|
+
// Bail if there's no relevant change
|
|
67
|
+
if (!(selectedLayer === null || selectedLayer === void 0 ? void 0 : selectedLayer.newValue)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const { newValue, oldValue } = selectedLayer;
|
|
71
|
+
// If layer was deleted (empty object) or the layer type changed, close the temporal controller
|
|
72
|
+
if (Object.keys(newValue).length === 0 ||
|
|
73
|
+
newValue.type !== oldValue.type) {
|
|
74
|
+
model.toggleTemporalController();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
// Initial state
|
|
78
|
+
handleClientStateChanged();
|
|
79
|
+
model.clientStateChanged.connect(handleClientStateChanged);
|
|
80
|
+
model.sharedLayersChanged.connect(handleLayerChange);
|
|
81
|
+
return () => {
|
|
82
|
+
model.clientStateChanged.disconnect(handleClientStateChanged);
|
|
83
|
+
model.sharedLayersChanged.disconnect(handleLayerChange);
|
|
84
|
+
removeFilter();
|
|
85
|
+
if (intervalRef.current) {
|
|
86
|
+
clearInterval(intervalRef.current);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}, []);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
layerIdRef.current = layerId;
|
|
92
|
+
}, [layerId]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const [key, set] of Object.entries(featureProperties)) {
|
|
96
|
+
if (set.size === 0) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const sampleValue = set.values().next().value;
|
|
100
|
+
// Validate value type
|
|
101
|
+
const isString = typeof sampleValue === 'string';
|
|
102
|
+
const isInteger = Number.isInteger(sampleValue);
|
|
103
|
+
if (!isString && !isInteger) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Date validation
|
|
107
|
+
if (isString) {
|
|
108
|
+
const dateFormatFromString = determineDateFormat(sampleValue);
|
|
109
|
+
if (!dateFormatFromString) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
setDateFormat(dateFormatFromString);
|
|
113
|
+
}
|
|
114
|
+
results.push(key);
|
|
115
|
+
}
|
|
116
|
+
// if we have state then remove the ms from the converted feature name
|
|
117
|
+
const currentState = filterStates[layerId];
|
|
118
|
+
const currentFeature = currentState === null || currentState === void 0 ? void 0 : currentState.feature.slice(0, -2);
|
|
119
|
+
setValidFeatures(results);
|
|
120
|
+
setSelectedFeature(currentFeature !== null && currentFeature !== void 0 ? currentFeature : results[0]);
|
|
121
|
+
}, [featureProperties]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
var _a, _b, _c;
|
|
124
|
+
if (!selectedFeature || !featureProperties[selectedFeature]) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Get and validate values
|
|
128
|
+
const valueSet = featureProperties[selectedFeature];
|
|
129
|
+
if (valueSet.size === 0) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const values = Array.from(valueSet);
|
|
133
|
+
const [firstValue] = values;
|
|
134
|
+
// Check the type of the first element
|
|
135
|
+
const isString = typeof firstValue === 'string';
|
|
136
|
+
let convertedValues;
|
|
137
|
+
if (isString) {
|
|
138
|
+
convertedValues = values.map(value => Date.parse(value)); // Convert all strings to milliseconds
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
convertedValues = values; // Keep numbers as they are
|
|
142
|
+
}
|
|
143
|
+
// Calculate min and max
|
|
144
|
+
const min = Math.min(...convertedValues);
|
|
145
|
+
const max = Math.max(...convertedValues);
|
|
146
|
+
// Get valid step options
|
|
147
|
+
const filteredSteps = Object.fromEntries(Object.entries(stepMap).filter(([_, val]) => val < max - min));
|
|
148
|
+
//using filter item as a state object to restore prev values
|
|
149
|
+
const currentState = filterStates[layerId];
|
|
150
|
+
const step = (_a = Object.values(filteredSteps).slice(-1)[0]) !== null && _a !== void 0 ? _a : stepMap.millisecond;
|
|
151
|
+
setValidSteps(filteredSteps);
|
|
152
|
+
setStep(step);
|
|
153
|
+
setMinMax({ min, max });
|
|
154
|
+
setRange({
|
|
155
|
+
start: (_b = currentState === null || currentState === void 0 ? void 0 : currentState.betweenMin) !== null && _b !== void 0 ? _b : min,
|
|
156
|
+
end: (_c = currentState === null || currentState === void 0 ? void 0 : currentState.betweenMax) !== null && _c !== void 0 ? _c : min + step
|
|
157
|
+
});
|
|
158
|
+
model.addFeatureAsMs(layerId, selectedFeature);
|
|
159
|
+
}, [selectedFeature]);
|
|
160
|
+
// minMax needs to be set before current value so the slider displays correctly
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const currentState = filterStates[layerId];
|
|
163
|
+
setCurrentValue(typeof (currentState === null || currentState === void 0 ? void 0 : currentState.value) === 'number' ? currentState.value : minMax.min);
|
|
164
|
+
}, [minMax]);
|
|
165
|
+
// Guess the date format from a date string
|
|
166
|
+
const determineDateFormat = (dateString) => {
|
|
167
|
+
for (const format of commonDateFormats) {
|
|
168
|
+
const parsedDate = parse(dateString, format, new Date());
|
|
169
|
+
if (isValid(parsedDate)) {
|
|
170
|
+
return format; // Return the format if the date is valid
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null; // Return null if no format matches
|
|
174
|
+
};
|
|
175
|
+
// Convert milliseconds back to the original date string format
|
|
176
|
+
const millisecondsToDateString = (milliseconds, dateFormat) => {
|
|
177
|
+
const date = new Date(milliseconds); // Create a Date object from milliseconds
|
|
178
|
+
return format(date, dateFormat); // Format back to the original string format
|
|
179
|
+
};
|
|
180
|
+
const handleChange = (e) => {
|
|
181
|
+
setCurrentValue(+e.target.value);
|
|
182
|
+
setRange({ start: +e.target.value, end: +e.target.value + step });
|
|
183
|
+
applyFilter(+e.target.value);
|
|
184
|
+
};
|
|
185
|
+
const applyFilter = (value) => {
|
|
186
|
+
var _a, _b;
|
|
187
|
+
const newFilter = {
|
|
188
|
+
feature: `${selectedFeature}ms`,
|
|
189
|
+
operator: 'between',
|
|
190
|
+
value: value,
|
|
191
|
+
betweenMin: value,
|
|
192
|
+
betweenMax: value + step
|
|
193
|
+
};
|
|
194
|
+
const layer = model.getLayer(layerId);
|
|
195
|
+
if (!layer) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const appliedFilters = ((_a = layer.filters) === null || _a === void 0 ? void 0 : _a.appliedFilters) || [];
|
|
199
|
+
const logicalOp = ((_b = layer.filters) === null || _b === void 0 ? void 0 : _b.logicalOp) || 'all';
|
|
200
|
+
// This is the only way to add a 'between' filter so
|
|
201
|
+
// find the index of the existing 'between' filter
|
|
202
|
+
const betweenFilterIndex = appliedFilters.findIndex(filter => filter.operator === 'between');
|
|
203
|
+
if (betweenFilterIndex !== -1) {
|
|
204
|
+
// If found, replace the existing filter
|
|
205
|
+
appliedFilters[betweenFilterIndex] = Object.assign({}, newFilter);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// If not found, add the new filter
|
|
209
|
+
appliedFilters.push(newFilter);
|
|
210
|
+
}
|
|
211
|
+
// Apply the updated filters to the layer
|
|
212
|
+
layer.filters = { logicalOp, appliedFilters };
|
|
213
|
+
model.triggerLayerUpdate(layerId, layer);
|
|
214
|
+
};
|
|
215
|
+
const removeFilter = () => {
|
|
216
|
+
var _a, _b;
|
|
217
|
+
const layer = model.getLayer(layerIdRef.current);
|
|
218
|
+
if (!layer) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const appliedFilters = ((_a = layer.filters) === null || _a === void 0 ? void 0 : _a.appliedFilters) || [];
|
|
222
|
+
const logicalOp = ((_b = layer.filters) === null || _b === void 0 ? void 0 : _b.logicalOp) || 'all';
|
|
223
|
+
// This is the only way to add a 'between' filter so
|
|
224
|
+
// find the index of the existing 'between' filter
|
|
225
|
+
const betweenFilterIndex = appliedFilters.findIndex(filter => filter.operator === 'between');
|
|
226
|
+
if (betweenFilterIndex !== -1) {
|
|
227
|
+
// If found, replace the existing filter
|
|
228
|
+
appliedFilters.splice(betweenFilterIndex, 1);
|
|
229
|
+
}
|
|
230
|
+
// Apply the updated filters to the layer
|
|
231
|
+
layer.filters = { logicalOp, appliedFilters };
|
|
232
|
+
model.triggerLayerUpdate(layerIdRef.current, layer);
|
|
233
|
+
};
|
|
234
|
+
const playAnimation = () => {
|
|
235
|
+
// Clear any existing interval first
|
|
236
|
+
if (intervalRef.current) {
|
|
237
|
+
clearInterval(intervalRef.current);
|
|
238
|
+
}
|
|
239
|
+
const incrementValue = () => {
|
|
240
|
+
setCurrentValue(prev => {
|
|
241
|
+
// Calculate next value with safety bounds
|
|
242
|
+
const nextValue = prev + step;
|
|
243
|
+
// Clear interval if we've reached the max
|
|
244
|
+
// step is subtracted to keep range values correct
|
|
245
|
+
if (nextValue >= minMax.max - step && intervalRef.current) {
|
|
246
|
+
clearInterval(intervalRef.current);
|
|
247
|
+
return minMax.max - step;
|
|
248
|
+
}
|
|
249
|
+
return nextValue;
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
// Start animation
|
|
253
|
+
intervalRef.current = setInterval(incrementValue, 1000 / fps);
|
|
254
|
+
};
|
|
255
|
+
const pauseAnimation = () => {
|
|
256
|
+
if (intervalRef.current) {
|
|
257
|
+
clearInterval(intervalRef.current);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
return (React.createElement("div", { className: "jp-gis-temporal-slider-container" },
|
|
261
|
+
React.createElement("div", { className: "jp-gis-temporal-slider-row" },
|
|
262
|
+
React.createElement("div", null,
|
|
263
|
+
React.createElement("label", { htmlFor: "time-feature-select" }, "Feature: "),
|
|
264
|
+
React.createElement("select", { id: "time-feature-select", onChange: e => {
|
|
265
|
+
setSelectedFeature(e.target.value);
|
|
266
|
+
} }, validFeatures.map(feature => {
|
|
267
|
+
return (React.createElement("option", { value: feature, selected: selectedFeature === feature }, feature));
|
|
268
|
+
}))),
|
|
269
|
+
React.createElement("div", null,
|
|
270
|
+
React.createElement("span", null, "Current Frame:"),
|
|
271
|
+
' ',
|
|
272
|
+
millisecondsToDateString(range.start, dateFormat),
|
|
273
|
+
" \u2264 ",
|
|
274
|
+
React.createElement("span", null, "t"),
|
|
275
|
+
" \u2264",
|
|
276
|
+
' ',
|
|
277
|
+
millisecondsToDateString(range.end, dateFormat))),
|
|
278
|
+
React.createElement("div", { className: "jp-gis-temporal-slider-row" },
|
|
279
|
+
React.createElement("div", { className: "jp-gis-temporal-slider-controls" },
|
|
280
|
+
React.createElement("div", { className: "jp-gis-temporal-slider-sub-controls" },
|
|
281
|
+
React.createElement(Button, { appearance: "neutral", scale: "medium", onClick: pauseAnimation },
|
|
282
|
+
React.createElement(FontAwesomeIcon, { icon: faPause })),
|
|
283
|
+
React.createElement(Button, { appearance: "neutral", scale: "medium", onClick: playAnimation },
|
|
284
|
+
React.createElement(FontAwesomeIcon, { icon: faPlay }))),
|
|
285
|
+
React.createElement("div", { className: "jp-gis-temporal-slider-sub-controls", style: { minWidth: 0 } },
|
|
286
|
+
React.createElement("label", { htmlFor: "fps-number-input" }, "FPS:"),
|
|
287
|
+
React.createElement("input", { name: "fps-number-input", type: "number", value: fps, onChange: e => setFps(+e.target.value) }))),
|
|
288
|
+
React.createElement("div", null,
|
|
289
|
+
React.createElement(Slider, { min: minMax.min, max: minMax.max - step, value: currentValue, valueAsNumber: currentValue, step: step, onChange: handleChange, className: "jp-gis-temporal-slider" }))),
|
|
290
|
+
React.createElement("div", { className: "jp-gis-temporal-slider-row" },
|
|
291
|
+
React.createElement("div", null,
|
|
292
|
+
React.createElement("span", null, "Range: "),
|
|
293
|
+
millisecondsToDateString(minMax.min, dateFormat),
|
|
294
|
+
" ",
|
|
295
|
+
React.createElement("span", null, "to "),
|
|
296
|
+
millisecondsToDateString(minMax.max, dateFormat)),
|
|
297
|
+
React.createElement("div", null,
|
|
298
|
+
React.createElement("label", { htmlFor: "time-step-select" }, "Step: "),
|
|
299
|
+
React.createElement("select", { id: "time-step-select", onChange: e => {
|
|
300
|
+
setStep(+e.target.value);
|
|
301
|
+
} }, Object.entries(validSteps).map(([key, val]) => (React.createElement("option", { key: key, selected: val === step, value: val }, key))))))));
|
|
302
|
+
};
|
|
303
|
+
export default TemporalSlider;
|