@vcmap/core 5.0.0-rc.4 → 5.0.0-rc.5

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.
Files changed (30) hide show
  1. package/index.d.ts +222 -65
  2. package/index.js +9 -1
  3. package/package.json +5 -10
  4. package/src/vcs/vcm/category/appBackedCategory.js +41 -0
  5. package/src/vcs/vcm/category/category.js +374 -0
  6. package/src/vcs/vcm/category/categoryCollection.js +145 -0
  7. package/src/vcs/vcm/context.js +73 -0
  8. package/src/vcs/vcm/layer/featureStore.js +2 -2
  9. package/src/vcs/vcm/layer/featureStoreChanges.js +89 -73
  10. package/src/vcs/vcm/layer/geojson.js +3 -5
  11. package/src/vcs/vcm/layer/geojsonHelpers.js +3 -3
  12. package/src/vcs/vcm/layer/tileProvider/mvtTileProvider.js +3 -3
  13. package/src/vcs/vcm/layer/tileProvider/staticGeojsonTileProvider.js +3 -3
  14. package/src/vcs/vcm/layer/tileProvider/urlTemplateTileProvider.js +3 -3
  15. package/src/vcs/vcm/layer/vectorHelpers.js +4 -4
  16. package/src/vcs/vcm/layer/wfs.js +5 -5
  17. package/src/vcs/vcm/oblique/ObliqueDataSet.js +7 -7
  18. package/src/vcs/vcm/util/clipping/clippingPlaneHelper.js +4 -4
  19. package/src/vcs/vcm/util/featureProvider/featureProviderHelpers.js +3 -4
  20. package/src/vcs/vcm/util/featureProvider/wmsFeatureProvider.js +11 -6
  21. package/src/vcs/vcm/util/featureconverter/extent3D.js +181 -0
  22. package/src/vcs/vcm/util/fetch.js +32 -0
  23. package/src/vcs/vcm/util/overrideCollection.js +224 -0
  24. package/src/vcs/vcm/util/style/declarativeStyleItem.js +2 -0
  25. package/src/vcs/vcm/util/style/styleFactory.js +1 -1
  26. package/src/vcs/vcm/util/style/styleItem.js +2 -0
  27. package/src/vcs/vcm/util/style/vectorStyleItem.js +2 -0
  28. package/src/vcs/vcm/vcsApp.js +360 -0
  29. package/src/vcs/vcm/vcsAppContextHelpers.js +108 -0
  30. package/src/vcs/vcm/util/featureconverter/extent3d.js +0 -154
@@ -0,0 +1,374 @@
1
+ import { check } from '@vcsuite/check';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { parseBoolean } from '@vcsuite/parsers';
4
+ import { Feature } from 'ol';
5
+ import { contextIdSymbol, destroyCollection, getObjectFromOptions } from '../vcsAppContextHelpers.js';
6
+ import makeOverrideCollection, { isOverrideCollection } from '../util/overrideCollection.js';
7
+ import VcsObject from '../object.js';
8
+ import Vector from '../layer/vector.js';
9
+ import IndexedCollection from '../util/indexedCollection.js';
10
+ import { parseGeoJSON, writeGeoJSONFeature } from '../layer/geojsonHelpers.js';
11
+ import Collection from '../util/collection.js';
12
+ import { VcsClassRegistry } from '../classRegistry.js';
13
+ import { getStyleOrDefaultStyle } from '../util/style/styleFactory.js';
14
+
15
+ /**
16
+ * @typedef {VcsObjectOptions} CategoryOptions
17
+ * @property {string|Object<string, string>} [title]
18
+ * @property {boolean} [typed=false]
19
+ * @property {string|undefined} [featureProperty]
20
+ * @property {VectorOptions} [layerOptions={}]
21
+ * @property {Array<Object>} [items] - items are not evaluated by the constructor but passed to parseItem during deserialization.
22
+ * @property {string} [keyProperty=name]
23
+ */
24
+
25
+ /**
26
+ * @type {*|string}
27
+ */
28
+ const categoryContextId = uuidv4();
29
+
30
+ /**
31
+ * @param {import("@vcmap/core").Vector} layer
32
+ * @param {VectorOptions} options
33
+ * @private
34
+ */
35
+ function assignLayerOptions(layer, options) {
36
+ if (options.style) {
37
+ layer.setStyle(getStyleOrDefaultStyle(options.style, layer.defaultStyle));
38
+ }
39
+
40
+ if (options.highlightStyle) {
41
+ const highlightStyle = getStyleOrDefaultStyle(options.highlightStyle, layer.highlightStyle);
42
+ layer.setHighlightStyle(/** @type {import("@vcmap/core").VectorStyleItem} */ (highlightStyle));
43
+ }
44
+
45
+ if (options.vectorProperties) {
46
+ layer.vectorProperties.setValues(options.vectorProperties);
47
+ }
48
+
49
+ if (options.zIndex != null) {
50
+ layer.zIndex = options.zIndex;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @param {string} key
56
+ * @param {T} value
57
+ * @param {T} defaultOption
58
+ * @param {T=} option
59
+ * @template {number|boolean|string} T
60
+ * @returns {void}
61
+ */
62
+ function checkMergeOptionOverride(key, value, defaultOption, option) {
63
+ const isOverride = option == null ? value !== defaultOption : option !== value;
64
+ if (isOverride) {
65
+ throw new Error(`Cannot merge options, values of ${key} do not match`);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * A category contains user based items and is a special container. The container should not be created directly, but via
71
+ * the requestCategory API on the categories collection. Do not use toJSON to retrieve the state of a category, since
72
+ * categories outlive contexts and may be changed with mergeOptions to no longer reflect your initial state. Requestors
73
+ * should keep track of the requested options themselves.
74
+ * @class
75
+ * @extends {VcsObject}
76
+ * @template {Object|VcsObject} T
77
+ */
78
+ class Category extends VcsObject {
79
+ static get className() { return 'category.Category'; }
80
+
81
+ /**
82
+ * @returns {CategoryOptions}
83
+ */
84
+ static getDefaultConfig() {
85
+ return {
86
+ title: '',
87
+ featureProperty: undefined,
88
+ typed: false,
89
+ layerOptions: {},
90
+ keyProperty: 'name',
91
+ items: [],
92
+ };
93
+ }
94
+
95
+ /**
96
+ * @param {CategoryOptions} options
97
+ */
98
+ constructor(options) {
99
+ super(options);
100
+ const defaultOptions = Category.getDefaultConfig();
101
+ /**
102
+ * @type {string|Object<string, string>}
103
+ */
104
+ this.title = options.title || this.name;
105
+ /**
106
+ * @type {import("@vcmap/core").VcsApp}
107
+ * @protected
108
+ */
109
+ this._app = null;
110
+ /**
111
+ * @type {string}
112
+ * @private
113
+ */
114
+ this._featureProperty = options.featureProperty || defaultOptions.featureProperty;
115
+ /**
116
+ * @type {boolean}
117
+ * @private
118
+ */
119
+ this._typed = parseBoolean(options.typed, defaultOptions.typed);
120
+ /**
121
+ * @type {VectorOptions}
122
+ * @private
123
+ */
124
+ this._layerOptions = options.layerOptions || defaultOptions.layerOptions;
125
+ /**
126
+ * @type {import("@vcmap/core").Vector}
127
+ * @protected
128
+ */
129
+ this._layer = null;
130
+ if (this._featureProperty) {
131
+ this._layer = new Vector(this._layerOptions);
132
+ this._layer[contextIdSymbol] = categoryContextId;
133
+ }
134
+ /**
135
+ * @type {string}
136
+ * @private
137
+ */
138
+ this._keyProperty = options.keyProperty || defaultOptions.keyProperty;
139
+ /**
140
+ * @type {Array<function():void>}
141
+ * @private
142
+ */
143
+ this._collectionListeners = [];
144
+ /**
145
+ * @type {OverrideCollection<T>}
146
+ * @private
147
+ */
148
+ this._collection = null;
149
+ this.setCollection(new IndexedCollection(this._keyProperty));
150
+ /**
151
+ * @type {function():void}
152
+ * @private
153
+ */
154
+ this._contextRemovedListener = () => {};
155
+ }
156
+
157
+ /**
158
+ * The collection of this category.
159
+ * @type {OverrideCollection<T>}
160
+ * @readonly
161
+ */
162
+ get collection() {
163
+ return this._collection;
164
+ }
165
+
166
+ /**
167
+ * Returns the layer of this collection. Caution, do not use the layer API to add or remove items.
168
+ * When adding items to the collection, the features are added to the layer async (timeout of 0), since there is weird behavior
169
+ * when removing and adding a feature with the same id in the same sync call.
170
+ * @type {import("@vcmap/core").Vector|null}
171
+ */
172
+ get layer() {
173
+ return this._layer;
174
+ }
175
+
176
+ /**
177
+ * @param {T} item
178
+ * @protected
179
+ */
180
+ _itemAdded(item) {
181
+ if (this._featureProperty) {
182
+ const id = item[this._keyProperty];
183
+ this._layer.removeFeaturesById([id]); // this may be a replacement.
184
+
185
+ const geoJsonFeature = item[this._featureProperty];
186
+ let feature;
187
+ if (geoJsonFeature instanceof Feature) {
188
+ feature = geoJsonFeature;
189
+ } else if (typeof geoJsonFeature === 'object') {
190
+ const { features } = parseGeoJSON(geoJsonFeature);
191
+ if (features[0]) { // XXX do we warn on feature collection?
192
+ feature = features[0];
193
+ }
194
+ }
195
+
196
+ if (feature) {
197
+ feature.setId(id);
198
+ setTimeout(() => { this._layer.addFeatures([feature]); }, 0); // We need to set a timeout, since removing and adding the feature in the same sync call leads to undefined behavior in OL TODO recheck in ol 6.11
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * @param {T} item
205
+ * @protected
206
+ */
207
+ _itemRemoved(item) {
208
+ if (this._featureProperty) {
209
+ this._layer.removeFeaturesById([item[this._keyProperty]]);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * @param {T} item
215
+ * @protected
216
+ */
217
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
218
+ _itemReplaced(item) {}
219
+
220
+ /**
221
+ * @param {T} item
222
+ * @protected
223
+ */
224
+ // eslint-disable-next-line class-methods-use-this,no-unused-vars
225
+ _itemMoved(item) {}
226
+
227
+ /**
228
+ * @returns {string}
229
+ * @private
230
+ */
231
+ _getDynamicContextId() {
232
+ if (!this._app) {
233
+ throw new Error('Cannot get dynamic context id, before setting the vcApp');
234
+ }
235
+ return this._app.dynamicContextId;
236
+ }
237
+
238
+ /**
239
+ * Throws if typed, featureProperty and keyProperty do not match. Merges other options.
240
+ * Only merges: style, highlightStyle, zIndex & vectorProperties from layerOptions.
241
+ * @param {CategoryOptions} options
242
+ */
243
+ mergeOptions(options) {
244
+ const defaultOptions = Category.getDefaultConfig();
245
+ checkMergeOptionOverride('typed', this._typed, defaultOptions.typed, options.typed);
246
+ checkMergeOptionOverride(
247
+ 'featureProperty',
248
+ this._featureProperty,
249
+ defaultOptions.featureProperty,
250
+ options.featureProperty,
251
+ );
252
+ checkMergeOptionOverride('keyProperty', this._keyProperty, defaultOptions.keyProperty, options.keyProperty);
253
+ this.title = options.title || this.title;
254
+ if (options.layerOptions && this._layer) {
255
+ assignLayerOptions(this._layer, options.layerOptions);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * When setting the category, it MUST use the same unqiueKey as the previous collection (default is "name").
261
+ * All items in the current collection _will be destroyed_ and the current collection will be destroyed. The category will take
262
+ * complete ownership of the collection and destroy it once the category is destroyed. The collection will
263
+ * be turned into an {@see OverrideCollection}.
264
+ * @param {import("@vcmap/core").Collection<T>} collection
265
+ */
266
+ setCollection(collection) {
267
+ check(collection, Collection);
268
+
269
+ if (this._keyProperty !== collection.uniqueKey) {
270
+ throw new Error('The collections key property does not match the categories key property');
271
+ }
272
+
273
+ this._collectionListeners.forEach((cb) => { cb(); });
274
+ if (this._collection) {
275
+ destroyCollection(this._collection);
276
+ }
277
+ if (this._layer) {
278
+ this._layer.removeAllFeatures(); // XXX should we call `itemRemoved` instead?
279
+ }
280
+
281
+ this._collection = collection[isOverrideCollection] ?
282
+ /** @type {OverrideCollection} */ (collection) :
283
+ makeOverrideCollection(
284
+ collection,
285
+ this._getDynamicContextId.bind(this),
286
+ this._serializeItem.bind(this),
287
+ this._typed ? getObjectFromOptions : null,
288
+ );
289
+
290
+ [...this.collection].forEach((item) => { this._itemAdded(item); });
291
+ /**
292
+ * @type {Array<function():void>}
293
+ * @private
294
+ */
295
+ this._collectionListeners = [
296
+ this._collection.added.addEventListener(this._itemAdded.bind(this)),
297
+ this._collection.removed.addEventListener(this._itemRemoved.bind(this)),
298
+ this._collection.replaced.addEventListener(this._itemReplaced.bind(this)),
299
+ ];
300
+
301
+ // @ts-ignore
302
+ if (this._collection.moved) {
303
+ // @ts-ignore
304
+ this._collectionListeners.push(this._collection.moved.addEventListener(this._itemMoved.bind(this)));
305
+ }
306
+ }
307
+
308
+ /**
309
+ * @param {import("@vcmap/core").VcsApp} app
310
+ */
311
+ setApp(app) {
312
+ if (this._app) {
313
+ throw new Error('Cannot switch apps');
314
+ }
315
+ this._app = app;
316
+ this._contextRemovedListener = this._app.contextRemoved.addEventListener((context) => {
317
+ this._collection.removeContext(context.id);
318
+ });
319
+ if (this._layer) {
320
+ this._app.layers.add(this._layer);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * @protected
326
+ * @param {T} item
327
+ * @returns {Array<Object>}
328
+ */
329
+ _serializeItem(item) {
330
+ const config = JSON.parse(JSON.stringify(item));
331
+ if (this._featureProperty) {
332
+ const feature = this._layer.getFeatureById(item[this._keyProperty]);
333
+ if (feature) {
334
+ config[this._featureProperty] = writeGeoJSONFeature(feature);
335
+ }
336
+ }
337
+ return config;
338
+ }
339
+
340
+ /**
341
+ * @param {string} contextId
342
+ * @returns {CategoryOptions|null}
343
+ */
344
+ serializeForContext(contextId) {
345
+ if (this._collection.size === 0) {
346
+ return null;
347
+ }
348
+
349
+ return {
350
+ name: this.name,
351
+ items: this.collection.serializeContext(contextId),
352
+ };
353
+ }
354
+
355
+ destroy() {
356
+ super.destroy();
357
+ if (this._app && this._layer) {
358
+ this._app.layers.remove(this._layer);
359
+ }
360
+ if (this._layer) {
361
+ this._layer.destroy();
362
+ }
363
+
364
+ this._collectionListeners.forEach((cb) => { cb(); });
365
+ this._collectionListeners.splice(0);
366
+ this._contextRemovedListener();
367
+ this._contextRemovedListener = () => {};
368
+ destroyCollection(this._collection);
369
+ this._app = null;
370
+ }
371
+ }
372
+
373
+ export default Category;
374
+ VcsClassRegistry.registerClass(Category.className, Category);
@@ -0,0 +1,145 @@
1
+ import { getLogger as getLoggerByName } from '@vcsuite/logger';
2
+ import Category from './category.js';
3
+ import { getObjectFromOptions } from '../vcsAppContextHelpers.js';
4
+ import './appBackedCategory.js';
5
+ import IndexedCollection from '../util/indexedCollection.js';
6
+
7
+ /**
8
+ * @returns {import("@vcsuite/logger").Logger}
9
+ */
10
+ function getLogger() {
11
+ return getLoggerByName('CategoryCollection');
12
+ }
13
+
14
+ /**
15
+ * @class
16
+ * @extends {IndexedCollection<Category<Object|import("@vcmap/core").VcsObject>>}
17
+ */
18
+ class CategoryCollection extends IndexedCollection {
19
+ /**
20
+ * @param {import("@vcmap/core").VcsApp} app
21
+ */
22
+ constructor(app) {
23
+ super();
24
+ /**
25
+ * @type {import("@vcmap/core").VcsApp}
26
+ * @private
27
+ */
28
+ this._app = app;
29
+ /**
30
+ * Map of category names, where the value is a map of contextId and items.
31
+ * @type {Map<string, Map<string, Array<Object>>>}
32
+ * @private
33
+ */
34
+ this._cache = new Map();
35
+ /**
36
+ * @type {Function}
37
+ * @private
38
+ */
39
+ this._contextRemovedListener = this._app.contextRemoved.addEventListener((context) => {
40
+ this._cache.forEach((contextMap, name) => {
41
+ contextMap.delete(context.id);
42
+ if (contextMap.size === 0) {
43
+ this._cache.delete(name);
44
+ }
45
+ });
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Do not call add directly. Use request category for adding categories.
51
+ * @param {Category<Object|import("@vcmap/core").VcsObject>} category
52
+ * @returns {number|null}
53
+ */
54
+ add(category) { // XXX use a symbol to enforce using request over add?
55
+ if (this.hasKey(category.name)) {
56
+ return null;
57
+ }
58
+
59
+ category.setApp(this._app);
60
+ const added = super.add(category);
61
+ if (added != null && this._cache.has(category.name)) {
62
+ this._cache
63
+ .get(category.name)
64
+ .forEach((items, contextId) => {
65
+ this.parseCategoryItems(category.name, items, contextId);
66
+ });
67
+
68
+ this._cache.delete(category.name);
69
+ }
70
+ return added;
71
+ }
72
+
73
+ /**
74
+ * Categories should be static. Removing them can lead to undefined behavior.
75
+ * @param {Category<Object|import("@vcmap/core").VcsObject>} category
76
+ */
77
+ remove(category) { // XXX add logger warning?
78
+ super.remove(category);
79
+ this._cache.delete(category.name);
80
+ }
81
+
82
+ /**
83
+ * Parses the category items. Items will only be parsed, if a category with said name exists. Otherwise,
84
+ * they will be cached, until such a category is requested.
85
+ * @param {string} name
86
+ * @param {Array<Object>} items
87
+ * @param {string} contextId
88
+ * @returns {Promise<void>}
89
+ */
90
+ async parseCategoryItems(name, items, contextId) {
91
+ const category = this.getByKey(name);
92
+
93
+ if (category) {
94
+ await category.collection.parseItems(items, contextId);
95
+ } else if (this._cache.has(name)) {
96
+ this._cache.get(name).set(contextId, items);
97
+ } else {
98
+ this._cache.set(name, new Map([[contextId, items]]));
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Add categories with this API.
104
+ * @param {CategoryOptions} options
105
+ * @returns {Promise<Category<Object|import("@vcmap/core").VcsObject>>}
106
+ */
107
+ async requestCategory(options) {
108
+ if (!options.name) {
109
+ getLogger().error('Cannot request a category without a name');
110
+ return null;
111
+ }
112
+
113
+ if (!options.type) {
114
+ getLogger().warning(`Implicitly typing category ${options.name} as ${Category.className}`);
115
+ options.type = Category.className;
116
+ }
117
+
118
+ let category;
119
+ if (this.hasKey(options.name)) {
120
+ category = this.getByKey(options.name);
121
+ category.mergeOptions(options);
122
+ } else {
123
+ category = await getObjectFromOptions(options);
124
+ if (category) {
125
+ if (this.add(category) == null) {
126
+ return null;
127
+ }
128
+ }
129
+ }
130
+
131
+ if (!category) {
132
+ throw new Error(`Category ${options.name} with type ${category.type} could not be created`);
133
+ }
134
+ return category;
135
+ }
136
+
137
+ destroy() {
138
+ super.destroy();
139
+ this._contextRemovedListener();
140
+ this._cache.clear();
141
+ this._app = null;
142
+ }
143
+ }
144
+
145
+ export default CategoryCollection;
@@ -0,0 +1,73 @@
1
+ import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
2
+
3
+ /**
4
+ * @typedef {Object} VcsAppConfig
5
+ * @property {string|undefined} [id]
6
+ * @property {Array<LayerOptions>} [layers]
7
+ * @property {Array<VcsMapOptions>} [maps]
8
+ * @property {Array<StyleItemOptions>} [styles]
9
+ * @property {Array<ViewPointOptions>} [viewpoints]
10
+ * @property {string} [startingViewPointName]
11
+ * @property {string} [startingMapName]
12
+ * @property {ProjectionOptions} [projection]
13
+ * @property {Array<{ name: string, items: Array<Object> }>} [categories]
14
+ * @property {Array<ObliqueCollectionOptions>} [obliqueCollections]
15
+ */
16
+
17
+ /**
18
+ * @type {string}
19
+ */
20
+ const uuidNamespace = uuidv4();
21
+
22
+ /**
23
+ * @class
24
+ * @export
25
+ */
26
+ class Context {
27
+ /**
28
+ * @param {VcsAppConfig} config
29
+ */
30
+ constructor(config) {
31
+ /**
32
+ * @type {VcsAppConfig}
33
+ * @private
34
+ */
35
+ this._config = config;
36
+ /**
37
+ * @type {string}
38
+ * @private
39
+ */
40
+ this._checkSum = uuidv5(JSON.stringify(config), uuidNamespace);
41
+ /**
42
+ * @type {string}
43
+ * @private
44
+ */
45
+ this._id = config.id || this._checkSum;
46
+ }
47
+
48
+ /**
49
+ * @type {string}
50
+ * @readonly
51
+ */
52
+ get id() {
53
+ return this._id;
54
+ }
55
+
56
+ /**
57
+ * @type {string}
58
+ * @readonly
59
+ */
60
+ get checkSum() {
61
+ return this._checkSum;
62
+ }
63
+
64
+ /**
65
+ * @type {VcsAppConfig}
66
+ * @readonly
67
+ */
68
+ get config() {
69
+ return JSON.parse(JSON.stringify(this._config));
70
+ }
71
+ }
72
+
73
+ export default Context;
@@ -1,4 +1,3 @@
1
- import axios from 'axios';
2
1
  import Feature from 'ol/Feature.js';
3
2
  import { Cesium3DTileFeature, Cesium3DTilePointFeature, ImagerySplitDirection } from '@vcmap/cesium';
4
3
  import VectorSource from 'ol/source/Vector.js';
@@ -29,6 +28,7 @@ import VectorOblique from './oblique/vectorOblique.js';
29
28
  import Extent from '../util/extent.js';
30
29
  import { isMobile } from '../util/isMobile.js';
31
30
  import { VcsClassRegistry } from '../classRegistry.js';
31
+ import { requestJson } from '../util/fetch.js';
32
32
 
33
33
  /**
34
34
  * @typedef {Object} FeatureStoreStaticRepresentation
@@ -249,7 +249,7 @@ class FeatureStore extends Vector {
249
249
  _loadTwoDim() {
250
250
  if (!this._twoDimLoaded) {
251
251
  this._twoDimLoaded = (async () => {
252
- const { data } = await axios.get(this.staticRepresentation.twoDim);
252
+ const data = await requestJson(this.staticRepresentation.twoDim);
253
253
  const { features } = parseGeoJSON(data, {
254
254
  targetProjection: mercatorProjection,
255
255
  dynamicStyle: true,