@kispace-io/gs-lib 1.1.8 → 1.2.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/dist/base-map-builder.d.ts.map +1 -1
- package/dist/gs-model.d.ts +6 -0
- package/dist/gs-model.d.ts.map +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +890 -288
- package/dist/index.js.map +1 -1
- package/dist/map-renderer.d.ts +94 -0
- package/dist/map-renderer.d.ts.map +1 -0
- package/dist/ml/gs-gs2ml.d.ts +96 -0
- package/dist/ml/gs-gs2ml.d.ts.map +1 -0
- package/dist/ml/gs-ml-adapters.d.ts +41 -0
- package/dist/ml/gs-ml-adapters.d.ts.map +1 -0
- package/dist/ml/gs-ml-lib.d.ts +17 -0
- package/dist/ml/gs-ml-lib.d.ts.map +1 -0
- package/dist/ml/gs-ml2gs.d.ts +10 -0
- package/dist/ml/gs-ml2gs.d.ts.map +1 -0
- package/dist/ml/gs-mlns.d.ts +10 -0
- package/dist/ml/gs-mlns.d.ts.map +1 -0
- package/dist/ml/index.d.ts +9 -0
- package/dist/ml/index.d.ts.map +1 -0
- package/dist/ml/maplibre-map-renderer.d.ts +66 -0
- package/dist/ml/maplibre-map-renderer.d.ts.map +1 -0
- package/dist/{gs-gs2ol.d.ts → ol/gs-gs2ol.d.ts} +2 -2
- package/dist/ol/gs-gs2ol.d.ts.map +1 -0
- package/dist/ol/gs-ol-adapters.d.ts.map +1 -0
- package/dist/{gs-lib.d.ts → ol/gs-ol-lib.d.ts} +4 -4
- package/dist/ol/gs-ol-lib.d.ts.map +1 -0
- package/dist/{gs-ol2gs.d.ts → ol/gs-ol2gs.d.ts} +1 -1
- package/dist/ol/gs-ol2gs.d.ts.map +1 -0
- package/dist/ol/gs-olns.d.ts.map +1 -0
- package/dist/ol/index.d.ts +9 -0
- package/dist/ol/index.d.ts.map +1 -0
- package/dist/ol/openlayers-map-renderer.d.ts +68 -0
- package/dist/ol/openlayers-map-renderer.d.ts.map +1 -0
- package/package.json +6 -2
- package/src/base-map-builder.ts +8 -9
- package/src/gs-model.ts +7 -1
- package/src/index.ts +12 -7
- package/src/map-renderer.ts +115 -0
- package/src/ml/gs-gs2ml.ts +717 -0
- package/src/ml/gs-ml-adapters.ts +134 -0
- package/src/ml/gs-ml-lib.ts +124 -0
- package/src/ml/gs-ml2gs.ts +66 -0
- package/src/ml/gs-mlns.ts +50 -0
- package/src/ml/index.ts +41 -0
- package/src/ml/maplibre-map-renderer.ts +428 -0
- package/src/{gs-gs2ol.ts → ol/gs-gs2ol.ts} +10 -4
- package/src/{gs-lib.ts → ol/gs-ol-lib.ts} +7 -6
- package/src/{gs-ol2gs.ts → ol/gs-ol2gs.ts} +1 -1
- package/src/ol/index.ts +21 -0
- package/src/ol/openlayers-map-renderer.ts +719 -0
- package/dist/gs-gs2ol.d.ts.map +0 -1
- package/dist/gs-lib.d.ts.map +0 -1
- package/dist/gs-ol-adapters.d.ts.map +0 -1
- package/dist/gs-ol2gs.d.ts.map +0 -1
- package/dist/gs-olns.d.ts.map +0 -1
- /package/dist/{gs-ol-adapters.d.ts → ol/gs-ol-adapters.d.ts} +0 -0
- /package/dist/{gs-olns.d.ts → ol/gs-olns.d.ts} +0 -0
- /package/src/{gs-ol-adapters.ts → ol/gs-ol-adapters.ts} +0 -0
- /package/src/{gs-olns.ts → ol/gs-olns.ts} +0 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { MapOperations, MapRenderer, MapSyncEvent, ScreenshotResult } from "../map-renderer";
|
|
2
|
+
import {
|
|
3
|
+
GsMap,
|
|
4
|
+
GsSourceType,
|
|
5
|
+
KEY_NAME,
|
|
6
|
+
KEY_STATE,
|
|
7
|
+
KEY_UUID,
|
|
8
|
+
GsFeature,
|
|
9
|
+
GsGeometry,
|
|
10
|
+
ensureUuid,
|
|
11
|
+
getStyleForFeature
|
|
12
|
+
} from "../gs-model";
|
|
13
|
+
import { toOlLayer, cleanupEventSubscriptions, toOlStyle } from "./gs-gs2ol";
|
|
14
|
+
import { toGsFeature } from "./gs-ol2gs";
|
|
15
|
+
import { olLib } from "./gs-ol-lib";
|
|
16
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
17
|
+
import {
|
|
18
|
+
Map as OlMap,
|
|
19
|
+
Feature,
|
|
20
|
+
style,
|
|
21
|
+
sphere,
|
|
22
|
+
geom,
|
|
23
|
+
layer as layerNS,
|
|
24
|
+
source as sourceNS,
|
|
25
|
+
interaction as interactionNS,
|
|
26
|
+
FeatureLike,
|
|
27
|
+
eventsCondition,
|
|
28
|
+
BaseLayer
|
|
29
|
+
} from "./gs-olns";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Lightweight helper to extract minimal GsFeature data for style evaluation
|
|
33
|
+
* Avoids the overhead of full toGsFeature() conversion
|
|
34
|
+
*/
|
|
35
|
+
function getFeatureStyleData(feature: Feature): GsFeature {
|
|
36
|
+
const geometry = feature.getGeometry() as geom.Geometry;
|
|
37
|
+
return ensureUuid({
|
|
38
|
+
geometry: ensureUuid({
|
|
39
|
+
type: geometry.getType(),
|
|
40
|
+
coordinates: [] // Not needed for style rules
|
|
41
|
+
} as GsGeometry),
|
|
42
|
+
state: feature.get(KEY_STATE)
|
|
43
|
+
} as GsFeature);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* OpenLayers map renderer that manages OpenLayers maps
|
|
48
|
+
* Provides complete isolation between the host app and the rendering engine
|
|
49
|
+
* User modules are handled by the toOlMap() function
|
|
50
|
+
*/
|
|
51
|
+
export class OpenLayersMapRenderer implements MapRenderer {
|
|
52
|
+
public olMap?: OlMap; // Made public for backward compatibility
|
|
53
|
+
gsMap: GsMap;
|
|
54
|
+
private env?: any;
|
|
55
|
+
private onDirtyCallback?: () => void;
|
|
56
|
+
private onSyncCallback?: (event: MapSyncEvent) => void;
|
|
57
|
+
private isDestroyed: boolean = false;
|
|
58
|
+
private operations?: OpenLayersMapOperations;
|
|
59
|
+
private styleCache: Map<string, style.Style> = new Map();
|
|
60
|
+
|
|
61
|
+
constructor(gsMap: GsMap, env?: any) {
|
|
62
|
+
this.gsMap = gsMap;
|
|
63
|
+
this.env = env;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async reattached(): Promise<void> {
|
|
67
|
+
// OpenLayers doesn't need special handling for reattachment
|
|
68
|
+
// The map stays attached to its container element
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async render(container: string | HTMLElement): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
// Use the runtime library to render the map
|
|
74
|
+
this.olMap = await olLib({
|
|
75
|
+
containerSelector: container,
|
|
76
|
+
gsMap: this.gsMap,
|
|
77
|
+
env: this.env,
|
|
78
|
+
mapOptions: {
|
|
79
|
+
controls: { zoom: false, attribution: false }
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Create operations after map is available
|
|
84
|
+
this.operations = new OpenLayersMapOperations(this.olMap, this);
|
|
85
|
+
|
|
86
|
+
// Apply styling to all vector layers
|
|
87
|
+
this.applyStylesToLayers();
|
|
88
|
+
|
|
89
|
+
// Set up event listeners after the map is rendered
|
|
90
|
+
this.olMap.once('rendercomplete', () => {
|
|
91
|
+
this.setupEventListeners();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Failed to render map:', error);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private applyStylesToLayers(): void {
|
|
101
|
+
if (!this.olMap) return;
|
|
102
|
+
|
|
103
|
+
const layers = this.olMap.getLayers().getArray();
|
|
104
|
+
layers.forEach(layer => {
|
|
105
|
+
if (layer instanceof layerNS.Vector) {
|
|
106
|
+
this.applyStyleToVectorLayer(layer);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private applyStyleToVectorLayer(layer: layerNS.Vector): void {
|
|
112
|
+
const layerName = layer.get(KEY_NAME);
|
|
113
|
+
|
|
114
|
+
// Create a style function that applies rules to each feature
|
|
115
|
+
const styleFunction = (feature: FeatureLike) => {
|
|
116
|
+
// Only process actual Feature objects (not RenderFeature)
|
|
117
|
+
if (!(feature instanceof Feature)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract minimal data needed for style evaluation (lightweight, no coordinate conversion)
|
|
122
|
+
const featureStyleData = getFeatureStyleData(feature);
|
|
123
|
+
const styleRules = this.gsMap.styleRules;
|
|
124
|
+
const stylesMap = this.gsMap.styles;
|
|
125
|
+
|
|
126
|
+
if (styleRules && stylesMap) {
|
|
127
|
+
const gsStyle = getStyleForFeature(featureStyleData, styleRules, stylesMap, layerName);
|
|
128
|
+
if (gsStyle && gsStyle.id) {
|
|
129
|
+
// Check cache first
|
|
130
|
+
let olStyle = this.styleCache.get(gsStyle.id);
|
|
131
|
+
if (!olStyle) {
|
|
132
|
+
// Convert and cache
|
|
133
|
+
olStyle = toOlStyle(gsStyle);
|
|
134
|
+
this.styleCache.set(gsStyle.id, olStyle);
|
|
135
|
+
}
|
|
136
|
+
return olStyle;
|
|
137
|
+
} else if (gsStyle) {
|
|
138
|
+
// Style without ID - can't cache, convert directly
|
|
139
|
+
return toOlStyle(gsStyle);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Return undefined to use default OpenLayers styling
|
|
144
|
+
return undefined;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
layer.setStyle(styleFunction);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private clearStyleCache(): void {
|
|
151
|
+
this.styleCache.clear();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async modelToUI(updatedGsMap?: GsMap): Promise<void> {
|
|
155
|
+
if (!this.olMap) {
|
|
156
|
+
throw new Error('Map not initialized');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Update the gsMap if provided
|
|
160
|
+
if (updatedGsMap) {
|
|
161
|
+
this.gsMap = updatedGsMap;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Clear style cache when model changes (styles/rules may have changed)
|
|
165
|
+
this.clearStyleCache();
|
|
166
|
+
|
|
167
|
+
// Get the container before destroying the map
|
|
168
|
+
const target = this.olMap.getTarget();
|
|
169
|
+
if (!target || typeof target === 'string') {
|
|
170
|
+
throw new Error('Map container not found or invalid');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.destroy();
|
|
174
|
+
|
|
175
|
+
// Clear the DOM container
|
|
176
|
+
target.innerHTML = '';
|
|
177
|
+
|
|
178
|
+
this.isDestroyed = false;
|
|
179
|
+
await this.render(target);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
getOperations(): MapOperations {
|
|
184
|
+
if (!this.operations) {
|
|
185
|
+
throw new Error("Operations not available - map not rendered yet");
|
|
186
|
+
}
|
|
187
|
+
return this.operations;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async getViewExtent(): Promise<number[]> {
|
|
191
|
+
console.debug("Getting view extent");
|
|
192
|
+
if (!this.olMap) {
|
|
193
|
+
throw new Error("Map not available for extent calculation");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const view = this.olMap.getView();
|
|
197
|
+
const extent = view.calculateExtent();
|
|
198
|
+
console.debug(`View extent: ${extent}`);
|
|
199
|
+
return extent;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async captureScreenshot(): Promise<ScreenshotResult> {
|
|
203
|
+
if (!this.olMap) {
|
|
204
|
+
return { success: false, error: 'Map not available' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const olMap = this.olMap;
|
|
208
|
+
|
|
209
|
+
// Wait for the map to finish rendering
|
|
210
|
+
await new Promise<void>((resolve) => {
|
|
211
|
+
olMap.renderSync();
|
|
212
|
+
olMap.once('rendercomplete', () => resolve());
|
|
213
|
+
// Fallback timeout in case rendercomplete doesn't fire
|
|
214
|
+
setTimeout(() => resolve(), 2000);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const size = olMap.getSize();
|
|
218
|
+
const width = size ? size[0] : olMap.getViewport().clientWidth;
|
|
219
|
+
const height = size ? size[1] : olMap.getViewport().clientHeight;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const canvas = olMap.getViewport().querySelector('canvas');
|
|
223
|
+
if (!canvas) {
|
|
224
|
+
return { success: false, error: 'Map canvas not found' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
228
|
+
return { success: true, dataUrl, width, height };
|
|
229
|
+
} catch (error: any) {
|
|
230
|
+
return { success: false, error: `Failed to capture canvas: ${error.message}` };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
setOnDirty(callback: () => void): void {
|
|
235
|
+
this.onDirtyCallback = callback;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
setOnSync(callback: (event: MapSyncEvent) => void): void {
|
|
239
|
+
this.onSyncCallback = callback;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
triggerDirty(): void {
|
|
243
|
+
if (this.isDestroyed || !this.onDirtyCallback) return;
|
|
244
|
+
this.onDirtyCallback();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
triggerSync(event: MapSyncEvent): void {
|
|
248
|
+
if (this.isDestroyed || !this.onSyncCallback) return;
|
|
249
|
+
this.onSyncCallback(event);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private syncViewToModel(): void {
|
|
253
|
+
if (!this.olMap) return;
|
|
254
|
+
|
|
255
|
+
const view = this.olMap.getView();
|
|
256
|
+
const center = view.getCenter();
|
|
257
|
+
const zoom = view.getZoom();
|
|
258
|
+
const rotation = view.getRotation();
|
|
259
|
+
|
|
260
|
+
// Notify host with specific view change event
|
|
261
|
+
if (center && zoom !== undefined) {
|
|
262
|
+
this.triggerSync({
|
|
263
|
+
type: 'viewChanged',
|
|
264
|
+
view: { center: center as [number, number], zoom, rotation }
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public syncLayerFeaturesToModel(layerUuid: string): void {
|
|
270
|
+
if (!this.olMap) return;
|
|
271
|
+
|
|
272
|
+
const layers = this.olMap.getLayers();
|
|
273
|
+
let olLayer: BaseLayer | undefined;
|
|
274
|
+
for (let i = 0; i < layers.getLength(); i++) {
|
|
275
|
+
const layer = layers.item(i);
|
|
276
|
+
if (layer.get(KEY_UUID) === layerUuid) {
|
|
277
|
+
olLayer = layer;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!olLayer || !(olLayer instanceof layerNS.Vector)) return;
|
|
283
|
+
|
|
284
|
+
const source = olLayer.getSource();
|
|
285
|
+
if (!source) return;
|
|
286
|
+
|
|
287
|
+
// Convert OpenLayers features back to GsFeatures
|
|
288
|
+
const olFeatures = source.getFeatures();
|
|
289
|
+
const gsFeatures = olFeatures.map((olFeature: Feature) => toGsFeature(olFeature));
|
|
290
|
+
|
|
291
|
+
// Notify host with features change event
|
|
292
|
+
this.triggerSync({
|
|
293
|
+
type: 'featuresChanged',
|
|
294
|
+
layerUuid,
|
|
295
|
+
features: gsFeatures
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private setupEventListeners(): void {
|
|
300
|
+
if (!this.olMap) return;
|
|
301
|
+
|
|
302
|
+
// Sync view changes back to domain model before triggering dirty
|
|
303
|
+
this.olMap.getView().on('change:center', () => {
|
|
304
|
+
this.syncViewToModel();
|
|
305
|
+
this.triggerDirty();
|
|
306
|
+
});
|
|
307
|
+
this.olMap.getView().on('change:resolution', () => {
|
|
308
|
+
this.syncViewToModel();
|
|
309
|
+
this.triggerDirty();
|
|
310
|
+
});
|
|
311
|
+
this.olMap.getView().on('change:rotation', () => {
|
|
312
|
+
this.syncViewToModel();
|
|
313
|
+
this.triggerDirty();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
this.olMap.getLayers().on('add', () => this.triggerDirty());
|
|
317
|
+
this.olMap.getLayers().on('remove', () => this.triggerDirty());
|
|
318
|
+
|
|
319
|
+
this.olMap.getControls().on('add', () => this.triggerDirty());
|
|
320
|
+
this.olMap.getControls().on('remove', () => this.triggerDirty());
|
|
321
|
+
|
|
322
|
+
this.olMap.getOverlays().on('add', () => this.triggerDirty());
|
|
323
|
+
this.olMap.getOverlays().on('remove', () => this.triggerDirty());
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
destroy(): void {
|
|
328
|
+
this.isDestroyed = true;
|
|
329
|
+
this.clearStyleCache();
|
|
330
|
+
// Cleanup operations (removes keyboard listeners) before destroying map
|
|
331
|
+
if (this.operations) {
|
|
332
|
+
const ops = this.operations as any;
|
|
333
|
+
if (ops.cleanup) {
|
|
334
|
+
ops.cleanup();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Cleanup event subscriptions before disposing map
|
|
338
|
+
if (this.olMap) {
|
|
339
|
+
cleanupEventSubscriptions(this.olMap);
|
|
340
|
+
}
|
|
341
|
+
this.olMap?.dispose();
|
|
342
|
+
this.olMap = undefined;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* OpenLayers-specific map operations implementation
|
|
349
|
+
*/
|
|
350
|
+
export class OpenLayersMapOperations implements MapOperations {
|
|
351
|
+
private drawInteraction?: interactionNS.Draw;
|
|
352
|
+
private selectInteraction?: interactionNS.Select;
|
|
353
|
+
private activeDrawingLayerUuid?: string;
|
|
354
|
+
private keyDownListener?: (event: KeyboardEvent) => void;
|
|
355
|
+
|
|
356
|
+
constructor(
|
|
357
|
+
private olMap: OlMap,
|
|
358
|
+
private renderer?: OpenLayersMapRenderer
|
|
359
|
+
) {
|
|
360
|
+
if (!olMap) {
|
|
361
|
+
throw new Error("OpenLayers map is required for operations");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Setup ESC key handler
|
|
365
|
+
this.keyDownListener = (event: KeyboardEvent) => {
|
|
366
|
+
if (event.key === 'Escape') {
|
|
367
|
+
if (this.drawInteraction) {
|
|
368
|
+
this.disableDrawing();
|
|
369
|
+
this.renderer?.triggerSync({ type: 'drawingDisabled' } as any);
|
|
370
|
+
}
|
|
371
|
+
if (this.selectInteraction) {
|
|
372
|
+
this.disableSelection();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const target = this.olMap.getTargetElement();
|
|
378
|
+
if (target && target instanceof HTMLElement) {
|
|
379
|
+
target.setAttribute('tabindex', '-1');
|
|
380
|
+
target.addEventListener('keydown', this.keyDownListener);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async setZoom(zoom: number): Promise<void> {
|
|
385
|
+
this.olMap.getView().setZoom(zoom);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async setCenter(center: [number, number]): Promise<void> {
|
|
389
|
+
this.olMap.getView().setCenter(center);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async switchColorMode(mode?: 'dark' | 'light'): Promise<void> {
|
|
393
|
+
const olMap = this.olMap;
|
|
394
|
+
let darkMode: boolean = olMap.get("darkmode") ?? false;
|
|
395
|
+
|
|
396
|
+
if (mode === 'dark') {
|
|
397
|
+
darkMode = true;
|
|
398
|
+
} else if (mode === 'light') {
|
|
399
|
+
darkMode = false;
|
|
400
|
+
} else {
|
|
401
|
+
darkMode = !darkMode;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
olMap.set("darkmode", darkMode);
|
|
405
|
+
|
|
406
|
+
const canvasElements = document.querySelectorAll('canvas');
|
|
407
|
+
canvasElements.forEach(canvas => {
|
|
408
|
+
canvas.style.filter = darkMode ? "invert(100%)" : "";
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
olMap.render();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async addLayer(layer: any, isBasemap?: boolean): Promise<void> {
|
|
415
|
+
const olLayer = toOlLayer(layer);
|
|
416
|
+
if (isBasemap) {
|
|
417
|
+
this.olMap.getLayers().insertAt(0, olLayer);
|
|
418
|
+
} else {
|
|
419
|
+
this.olMap.getLayers().push(olLayer);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async deleteLayer(uuid: string): Promise<void> {
|
|
424
|
+
const layers = this.olMap.getLayers();
|
|
425
|
+
for (let i = 0; i < layers.getLength(); i++) {
|
|
426
|
+
const layer = layers.item(i);
|
|
427
|
+
if (layer.get(KEY_UUID) === uuid) {
|
|
428
|
+
layers.removeAt(i);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async renameLayer(uuid: string, newName: string): Promise<void> {
|
|
435
|
+
const layers = this.olMap.getLayers();
|
|
436
|
+
for (let i = 0; i < layers.getLength(); i++) {
|
|
437
|
+
const layer = layers.item(i);
|
|
438
|
+
if (layer.get(KEY_UUID) === uuid) {
|
|
439
|
+
layer.set(KEY_NAME, newName);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async moveLayer(uuid: string, targetUuid?: string): Promise<void> {
|
|
446
|
+
const layers = this.olMap.getLayers();
|
|
447
|
+
let fromIndex = -1;
|
|
448
|
+
let toIndex = -1;
|
|
449
|
+
|
|
450
|
+
for (let i = 0; i < layers.getLength(); i++) {
|
|
451
|
+
const layer = layers.item(i);
|
|
452
|
+
if (layer.get(KEY_UUID) === uuid) {
|
|
453
|
+
fromIndex = i;
|
|
454
|
+
}
|
|
455
|
+
if (targetUuid && layer.get(KEY_UUID) === targetUuid) {
|
|
456
|
+
toIndex = i;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (fromIndex < 0) return;
|
|
461
|
+
|
|
462
|
+
if (targetUuid) {
|
|
463
|
+
if (toIndex < 0 || fromIndex === toIndex) return;
|
|
464
|
+
} else {
|
|
465
|
+
toIndex = fromIndex > 0 ? fromIndex - 1 : fromIndex + 1;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (toIndex >= 0 && toIndex < layers.getLength() && fromIndex !== toIndex) {
|
|
469
|
+
const layer = layers.item(fromIndex);
|
|
470
|
+
layers.removeAt(fromIndex);
|
|
471
|
+
layers.insertAt(toIndex, layer);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async setLayerVisible(uuid: string, visible: boolean): Promise<void> {
|
|
476
|
+
const layers = this.olMap.getLayers();
|
|
477
|
+
for (let i = 0; i < layers.getLength(); i++) {
|
|
478
|
+
const layer = layers.item(i);
|
|
479
|
+
if (layer.get(KEY_UUID) === uuid) {
|
|
480
|
+
layer.setVisible(visible);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async addControlFromModule(_src: string): Promise<void> {}
|
|
487
|
+
async removeControl(_uuid: string): Promise<void> {}
|
|
488
|
+
async addOverlayFromModule(_src: string, _position?: string): Promise<void> {}
|
|
489
|
+
async removeOverlay(_uuid: string): Promise<void> {}
|
|
490
|
+
|
|
491
|
+
private setCursor(cursor: string): void {
|
|
492
|
+
const viewport = this.olMap.getViewport();
|
|
493
|
+
if (viewport) {
|
|
494
|
+
(viewport as HTMLElement).style.cursor = cursor;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async enableDrawing(geometryType: 'Point' | 'LineString' | 'Polygon', layerUuid: string): Promise<void> {
|
|
499
|
+
this.disableSelection();
|
|
500
|
+
|
|
501
|
+
if (this.drawInteraction) {
|
|
502
|
+
this.olMap.removeInteraction(this.drawInteraction);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
this.activeDrawingLayerUuid = layerUuid;
|
|
506
|
+
this.setCursor('crosshair');
|
|
507
|
+
|
|
508
|
+
const layers = this.olMap.getLayers();
|
|
509
|
+
let layer: BaseLayer | undefined;
|
|
510
|
+
for (let i = 0; i < layers.getLength(); i++) {
|
|
511
|
+
const l = layers.item(i);
|
|
512
|
+
if (l.get(KEY_UUID) === layerUuid) {
|
|
513
|
+
layer = l;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (!layer || !(layer instanceof layerNS.Vector)) {
|
|
519
|
+
throw new Error('Drawing only supported on vector layers');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const source = layer.getSource();
|
|
523
|
+
if (!source) {
|
|
524
|
+
throw new Error('Layer has no source');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const layerSourceType = layer.get('sourceType');
|
|
528
|
+
if (layerSourceType && layerSourceType !== GsSourceType.Features) {
|
|
529
|
+
throw new Error('Drawing only supported on layers with in-memory features');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
this.drawInteraction = new interactionNS.Draw({
|
|
533
|
+
source: source,
|
|
534
|
+
type: geometryType
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const onFeatureAdded = (event: any) => {
|
|
538
|
+
const feature = event.feature;
|
|
539
|
+
if (feature && !feature.get(KEY_UUID)) {
|
|
540
|
+
const uuid = uuidv4();
|
|
541
|
+
feature.set(KEY_UUID, uuid);
|
|
542
|
+
const state = feature.get(KEY_STATE) || {};
|
|
543
|
+
state.uuid = uuid;
|
|
544
|
+
feature.set(KEY_STATE, state);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (this.renderer && this.activeDrawingLayerUuid) {
|
|
548
|
+
this.renderer.syncLayerFeaturesToModel(this.activeDrawingLayerUuid);
|
|
549
|
+
}
|
|
550
|
+
this.renderer?.triggerDirty();
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
source.on('addfeature', onFeatureAdded);
|
|
554
|
+
|
|
555
|
+
(this.drawInteraction as any)._featureAddedListener = onFeatureAdded;
|
|
556
|
+
(this.drawInteraction as any)._sourceRef = source;
|
|
557
|
+
|
|
558
|
+
this.olMap.addInteraction(this.drawInteraction);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async disableDrawing(): Promise<void> {
|
|
562
|
+
if (this.drawInteraction) {
|
|
563
|
+
const listener = (this.drawInteraction as any)._featureAddedListener;
|
|
564
|
+
const source = (this.drawInteraction as any)._sourceRef;
|
|
565
|
+
if (listener && source) {
|
|
566
|
+
source.un('addfeature', listener);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
this.olMap.removeInteraction(this.drawInteraction);
|
|
570
|
+
this.drawInteraction = undefined;
|
|
571
|
+
this.setCursor('');
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
cleanup(): void {
|
|
576
|
+
if (this.keyDownListener) {
|
|
577
|
+
const target = this.olMap.getTargetElement();
|
|
578
|
+
if (target && target instanceof HTMLElement) {
|
|
579
|
+
target.removeEventListener('keydown', this.keyDownListener);
|
|
580
|
+
}
|
|
581
|
+
this.keyDownListener = undefined;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async enableFeatureSelection(): Promise<void> {
|
|
586
|
+
this.disableDrawing();
|
|
587
|
+
this.disableSelection();
|
|
588
|
+
|
|
589
|
+
const olLayers = this.olMap.getLayers();
|
|
590
|
+
const vectorLayers = olLayers.getArray().filter(layer => layer instanceof layerNS.Vector) as layerNS.Vector<sourceNS.Vector>[];
|
|
591
|
+
|
|
592
|
+
if (vectorLayers.length === 0) {
|
|
593
|
+
throw new Error('No vector layers available for selection');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const gsMap = this.renderer?.gsMap;
|
|
597
|
+
const selectionStyle = gsMap?.styles?.['selection'];
|
|
598
|
+
|
|
599
|
+
const selectOptions: any = {
|
|
600
|
+
condition: eventsCondition.click,
|
|
601
|
+
layers: vectorLayers,
|
|
602
|
+
hitTolerance: 5,
|
|
603
|
+
style: selectionStyle ? toOlStyle(selectionStyle) : (_feature: any) => {
|
|
604
|
+
const stroke = new style.Stroke({ color: 'rgba(255, 255, 0, 1)', width: 3 });
|
|
605
|
+
const fill = new style.Fill({ color: 'rgba(255, 255, 0, 0.3)' });
|
|
606
|
+
return new style.Style({
|
|
607
|
+
image: new style.Circle({ radius: 7, fill: fill, stroke: stroke }),
|
|
608
|
+
stroke: stroke,
|
|
609
|
+
fill: fill
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
this.selectInteraction = new interactionNS.Select(selectOptions);
|
|
615
|
+
|
|
616
|
+
this.selectInteraction.on('select', (event: any) => {
|
|
617
|
+
if (event.selected.length > 0) {
|
|
618
|
+
const selectedFeature = event.selected[0];
|
|
619
|
+
let featureLayerUuid: string | undefined;
|
|
620
|
+
olLayers.getArray().forEach((layer) => {
|
|
621
|
+
if (layer instanceof layerNS.Vector) {
|
|
622
|
+
const source = layer.getSource();
|
|
623
|
+
if (source && source.hasFeature(selectedFeature)) {
|
|
624
|
+
const uuid = layer.get(KEY_UUID);
|
|
625
|
+
if (uuid) featureLayerUuid = uuid;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
if (featureLayerUuid && this.renderer) {
|
|
631
|
+
const gsFeature = toGsFeature(selectedFeature);
|
|
632
|
+
const geometry = selectedFeature.getGeometry();
|
|
633
|
+
const metrics: { length?: number, area?: number } = {};
|
|
634
|
+
|
|
635
|
+
if (geometry) {
|
|
636
|
+
const geometryType = geometry.getType();
|
|
637
|
+
try {
|
|
638
|
+
if (geometryType === 'LineString' || geometryType === 'MultiLineString') {
|
|
639
|
+
metrics.length = sphere.getLength(geometry, { projection: this.olMap.getView().getProjection() });
|
|
640
|
+
} else if (geometryType === 'Polygon' || geometryType === 'MultiPolygon') {
|
|
641
|
+
metrics.area = sphere.getArea(geometry, { projection: this.olMap.getView().getProjection() });
|
|
642
|
+
const coordinates = geometryType === 'Polygon'
|
|
643
|
+
? (geometry as any).getCoordinates()[0]
|
|
644
|
+
: (geometry as any).getCoordinates()[0][0];
|
|
645
|
+
if (coordinates?.length > 0) {
|
|
646
|
+
const perimeterLine = new geom.LineString(coordinates);
|
|
647
|
+
metrics.length = sphere.getLength(perimeterLine, { projection: this.olMap.getView().getProjection() });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.warn('Error calculating feature metrics:', error);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
this.renderer.triggerSync({
|
|
656
|
+
type: 'featureSelected',
|
|
657
|
+
layerUuid: featureLayerUuid,
|
|
658
|
+
feature: gsFeature,
|
|
659
|
+
metrics
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
} else if (event.deselected.length > 0) {
|
|
663
|
+
this.renderer?.triggerSync({ type: 'featureDeselected' });
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
this.olMap.addInteraction(this.selectInteraction);
|
|
668
|
+
this.setCursor('pointer');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async deleteSelectedFeatures(): Promise<void> {
|
|
672
|
+
if (!this.selectInteraction) {
|
|
673
|
+
throw new Error('No selection interaction active');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const selectedFeatures = this.selectInteraction.getFeatures();
|
|
677
|
+
|
|
678
|
+
if (selectedFeatures.getLength() === 0) {
|
|
679
|
+
throw new Error('No features selected');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const layersToSync = new Set<string>();
|
|
683
|
+
const olLayers = this.olMap.getLayers();
|
|
684
|
+
|
|
685
|
+
selectedFeatures.forEach((feature: any) => {
|
|
686
|
+
for (let i = 0; i < olLayers.getLength(); i++) {
|
|
687
|
+
const layer = olLayers.item(i);
|
|
688
|
+
if (layer instanceof layerNS.Vector) {
|
|
689
|
+
const source = layer.getSource();
|
|
690
|
+
if (source && source.hasFeature(feature)) {
|
|
691
|
+
source.removeFeature(feature);
|
|
692
|
+
const layerUuid = layer.get(KEY_UUID);
|
|
693
|
+
if (layerUuid) layersToSync.add(layerUuid);
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
selectedFeatures.clear();
|
|
701
|
+
|
|
702
|
+
if (this.renderer && layersToSync.size > 0) {
|
|
703
|
+
layersToSync.forEach(layerUuid => {
|
|
704
|
+
this.renderer!.syncLayerFeaturesToModel(layerUuid);
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
this.renderer?.triggerDirty();
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async disableSelection(): Promise<void> {
|
|
711
|
+
if (this.selectInteraction) {
|
|
712
|
+
this.olMap.removeInteraction(this.selectInteraction);
|
|
713
|
+
this.selectInteraction = undefined;
|
|
714
|
+
this.setCursor('');
|
|
715
|
+
this.renderer?.triggerSync({ type: 'featureDeselected' });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|