@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,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MapLibre GL map renderer implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { MapOperations, MapRenderer, MapSyncEvent, ScreenshotResult } from '../map-renderer';
|
|
6
|
+
import {
|
|
7
|
+
GsMap,
|
|
8
|
+
GsSourceType,
|
|
9
|
+
GsLayerType,
|
|
10
|
+
GsFeature,
|
|
11
|
+
GsGeometry,
|
|
12
|
+
KEY_UUID,
|
|
13
|
+
ensureUuid
|
|
14
|
+
} from '../gs-model';
|
|
15
|
+
import { mlLib } from './gs-ml-lib';
|
|
16
|
+
import {
|
|
17
|
+
toGeoJsonFeatureCollection,
|
|
18
|
+
toMlFillPaint,
|
|
19
|
+
toMlLinePaint,
|
|
20
|
+
toMlCirclePaint,
|
|
21
|
+
toWgs84,
|
|
22
|
+
toWebMercator,
|
|
23
|
+
ML_KEY_GS_LAYER_UUID
|
|
24
|
+
} from './gs-gs2ml';
|
|
25
|
+
import { toGsFeature } from './gs-ml2gs';
|
|
26
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
27
|
+
import { Map, MapMouseEvent, GeoJSONSource, LngLatLike } from 'maplibre-gl';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* MapLibre map renderer that implements the MapRenderer interface
|
|
31
|
+
*/
|
|
32
|
+
export class MapLibreMapRenderer implements MapRenderer {
|
|
33
|
+
private map?: Map;
|
|
34
|
+
private gsMap: GsMap;
|
|
35
|
+
private env?: any;
|
|
36
|
+
private onDirtyCallback?: () => void;
|
|
37
|
+
private onSyncCallback?: (event: MapSyncEvent) => void;
|
|
38
|
+
private isDestroyed: boolean = false;
|
|
39
|
+
private operations?: MapLibreMapOperations;
|
|
40
|
+
|
|
41
|
+
constructor(gsMap: GsMap, env?: any) {
|
|
42
|
+
this.gsMap = gsMap;
|
|
43
|
+
this.env = env;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async reattached(): Promise<void> {}
|
|
47
|
+
|
|
48
|
+
async render(container: string | HTMLElement): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
this.map = await mlLib({
|
|
51
|
+
containerSelector: container,
|
|
52
|
+
gsMap: this.gsMap,
|
|
53
|
+
env: this.env
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.operations = new MapLibreMapOperations(this.map, this);
|
|
57
|
+
this.setupEventListeners();
|
|
58
|
+
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Failed to render map:', error);
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async modelToUI(updatedGsMap?: GsMap): Promise<void> {
|
|
66
|
+
if (!this.map) throw new Error('Map not initialized');
|
|
67
|
+
if (updatedGsMap) this.gsMap = updatedGsMap;
|
|
68
|
+
|
|
69
|
+
const container = this.map.getContainer();
|
|
70
|
+
if (!container) throw new Error('Map container not found');
|
|
71
|
+
|
|
72
|
+
this.destroy();
|
|
73
|
+
container.innerHTML = '';
|
|
74
|
+
this.isDestroyed = false;
|
|
75
|
+
await this.render(container);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getOperations(): MapOperations {
|
|
79
|
+
if (!this.operations) throw new Error('Operations not available - map not rendered yet');
|
|
80
|
+
return this.operations;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getViewExtent(): Promise<number[]> {
|
|
84
|
+
if (!this.map) throw new Error('Map not available for extent calculation');
|
|
85
|
+
const bounds = this.map.getBounds();
|
|
86
|
+
return [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async captureScreenshot(): Promise<ScreenshotResult> {
|
|
90
|
+
if (!this.map) return { success: false, error: 'Map not available' };
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
this.map.triggerRepaint();
|
|
94
|
+
await new Promise(resolve => requestAnimationFrame(resolve));
|
|
95
|
+
|
|
96
|
+
const canvas = this.map.getCanvas();
|
|
97
|
+
if (!canvas) return { success: false, error: 'Map canvas not found' };
|
|
98
|
+
|
|
99
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
100
|
+
return { success: true, dataUrl, width: canvas.width, height: canvas.height };
|
|
101
|
+
} catch (error: any) {
|
|
102
|
+
return { success: false, error: `Failed to capture canvas: ${error.message}` };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setOnDirty(callback: () => void): void { this.onDirtyCallback = callback; }
|
|
107
|
+
setOnSync(callback: (event: MapSyncEvent) => void): void { this.onSyncCallback = callback; }
|
|
108
|
+
|
|
109
|
+
triggerDirty(): void {
|
|
110
|
+
if (this.isDestroyed || !this.onDirtyCallback) return;
|
|
111
|
+
this.onDirtyCallback();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
triggerSync(event: MapSyncEvent): void {
|
|
115
|
+
if (this.isDestroyed || !this.onSyncCallback) return;
|
|
116
|
+
this.onSyncCallback(event);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private syncViewToModel(): void {
|
|
120
|
+
if (!this.map) return;
|
|
121
|
+
const center = this.map.getCenter();
|
|
122
|
+
const zoom = this.map.getZoom();
|
|
123
|
+
const pitch = this.map.getPitch();
|
|
124
|
+
const bearing = this.map.getBearing();
|
|
125
|
+
const centerMercator = toWebMercator([center.lng, center.lat]);
|
|
126
|
+
|
|
127
|
+
this.triggerSync({
|
|
128
|
+
type: 'viewChanged',
|
|
129
|
+
view: { center: centerMercator, zoom, pitch, bearing }
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
syncLayerFeaturesToModel(layerUuid: string): void {
|
|
134
|
+
if (!this.map) return;
|
|
135
|
+
|
|
136
|
+
const source = this.map.getSource(layerUuid) as GeoJSONSource;
|
|
137
|
+
if (!source) return;
|
|
138
|
+
|
|
139
|
+
const features = this.map.querySourceFeatures(layerUuid);
|
|
140
|
+
const gsFeatures = features.map(f => toGsFeature(f as unknown as GeoJSON.Feature));
|
|
141
|
+
|
|
142
|
+
this.triggerSync({ type: 'featuresChanged', layerUuid, features: gsFeatures });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private setupEventListeners(): void {
|
|
146
|
+
if (!this.map) return;
|
|
147
|
+
|
|
148
|
+
this.map.on('moveend', () => {
|
|
149
|
+
this.syncViewToModel();
|
|
150
|
+
this.triggerDirty();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.map.on('zoomend', () => {
|
|
154
|
+
this.syncViewToModel();
|
|
155
|
+
this.triggerDirty();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.map.on('pitchend', () => {
|
|
159
|
+
this.syncViewToModel();
|
|
160
|
+
this.triggerDirty();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.map.on('rotateend', () => {
|
|
164
|
+
this.syncViewToModel();
|
|
165
|
+
this.triggerDirty();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getMap(): Map | undefined { return this.map; }
|
|
170
|
+
getGsMap(): GsMap { return this.gsMap; }
|
|
171
|
+
|
|
172
|
+
destroy(): void {
|
|
173
|
+
this.isDestroyed = true;
|
|
174
|
+
if (this.operations) this.operations.cleanup();
|
|
175
|
+
this.map?.remove();
|
|
176
|
+
this.map = undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* MapLibre-specific map operations implementation
|
|
182
|
+
*/
|
|
183
|
+
export class MapLibreMapOperations implements MapOperations {
|
|
184
|
+
private selectedFeatureId?: string | number;
|
|
185
|
+
private selectedLayerId?: string;
|
|
186
|
+
private drawMode?: 'Point' | 'LineString' | 'Polygon';
|
|
187
|
+
private clickHandler?: (e: MapMouseEvent) => void;
|
|
188
|
+
private keyHandler?: (e: KeyboardEvent) => void;
|
|
189
|
+
|
|
190
|
+
constructor(private map: Map, private renderer: MapLibreMapRenderer) {
|
|
191
|
+
this.keyHandler = (e: KeyboardEvent) => {
|
|
192
|
+
if (e.key === 'Escape') {
|
|
193
|
+
if (this.drawMode) {
|
|
194
|
+
this.disableDrawing();
|
|
195
|
+
this.renderer.triggerSync({ type: 'drawingDisabled' } as any);
|
|
196
|
+
}
|
|
197
|
+
if (this.selectedFeatureId !== undefined) {
|
|
198
|
+
this.disableSelection();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
document.addEventListener('keydown', this.keyHandler);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async setZoom(zoom: number): Promise<void> { this.map.setZoom(zoom); }
|
|
206
|
+
|
|
207
|
+
async setCenter(center: [number, number]): Promise<void> {
|
|
208
|
+
const centerWgs84 = toWgs84(center);
|
|
209
|
+
this.map.setCenter(centerWgs84 as LngLatLike);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async switchColorMode(mode?: 'dark' | 'light'): Promise<void> {
|
|
213
|
+
let darkMode: boolean = (this.map as any)._darkMode ?? false;
|
|
214
|
+
|
|
215
|
+
if (mode === 'dark') darkMode = true;
|
|
216
|
+
else if (mode === 'light') darkMode = false;
|
|
217
|
+
else darkMode = !darkMode;
|
|
218
|
+
|
|
219
|
+
(this.map as any)._darkMode = darkMode;
|
|
220
|
+
const canvas = this.map.getCanvas();
|
|
221
|
+
if (canvas) canvas.style.filter = darkMode ? 'invert(100%)' : '';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async addLayer(layer: any, isBasemap?: boolean): Promise<void> {
|
|
225
|
+
const sourceId = layer.uuid;
|
|
226
|
+
|
|
227
|
+
let sourceSpec: any;
|
|
228
|
+
if (layer.source.type === GsSourceType.Features) {
|
|
229
|
+
sourceSpec = { type: 'geojson', data: toGeoJsonFeatureCollection(layer.source.features || []) };
|
|
230
|
+
} else if (layer.source.type === GsSourceType.GeoJSON) {
|
|
231
|
+
sourceSpec = { type: 'geojson', data: layer.source.url };
|
|
232
|
+
} else if (layer.source.type === GsSourceType.OSM) {
|
|
233
|
+
sourceSpec = { type: 'raster', tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], tileSize: 256 };
|
|
234
|
+
} else if (layer.source.type === GsSourceType.XYZ) {
|
|
235
|
+
sourceSpec = { type: 'raster', tiles: [layer.source.url], tileSize: 256 };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (sourceSpec) {
|
|
239
|
+
this.map.addSource(sourceId, sourceSpec);
|
|
240
|
+
|
|
241
|
+
if (layer.type === GsLayerType.TILE) {
|
|
242
|
+
this.map.addLayer({
|
|
243
|
+
id: sourceId,
|
|
244
|
+
type: 'raster',
|
|
245
|
+
source: sourceId
|
|
246
|
+
}, isBasemap ? this.getFirstLayerId() : undefined);
|
|
247
|
+
} else if (layer.type === GsLayerType.VECTOR) {
|
|
248
|
+
this.map.addLayer({ id: `${sourceId}-fill`, type: 'fill', source: sourceId,
|
|
249
|
+
filter: ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']],
|
|
250
|
+
paint: toMlFillPaint() });
|
|
251
|
+
this.map.addLayer({ id: `${sourceId}-line`, type: 'line', source: sourceId,
|
|
252
|
+
filter: ['any', ['==', ['geometry-type'], 'LineString'], ['==', ['geometry-type'], 'MultiLineString']],
|
|
253
|
+
paint: toMlLinePaint() });
|
|
254
|
+
this.map.addLayer({ id: `${sourceId}-circle`, type: 'circle', source: sourceId,
|
|
255
|
+
filter: ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']],
|
|
256
|
+
paint: toMlCirclePaint() });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private getFirstLayerId(): string | undefined {
|
|
262
|
+
return this.map.getStyle()?.layers?.[0]?.id;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async deleteLayer(uuid: string): Promise<void> {
|
|
266
|
+
const suffixes = ['', '-fill', '-line', '-circle', '-extrusion'];
|
|
267
|
+
for (const suffix of suffixes) {
|
|
268
|
+
const layerId = `${uuid}${suffix}`;
|
|
269
|
+
if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
|
|
270
|
+
}
|
|
271
|
+
if (this.map.getSource(uuid)) this.map.removeSource(uuid);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async renameLayer(_uuid: string, _newName: string): Promise<void> {}
|
|
275
|
+
|
|
276
|
+
async moveLayer(uuid: string, targetUuid?: string): Promise<void> {
|
|
277
|
+
const suffixes = ['', '-fill', '-line', '-circle', '-extrusion'];
|
|
278
|
+
for (const suffix of suffixes) {
|
|
279
|
+
const layerId = `${uuid}${suffix}`;
|
|
280
|
+
const beforeId = targetUuid ? `${targetUuid}${suffix}` : undefined;
|
|
281
|
+
if (this.map.getLayer(layerId)) this.map.moveLayer(layerId, beforeId);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async setLayerVisible(uuid: string, visible: boolean): Promise<void> {
|
|
286
|
+
const visibility = visible ? 'visible' : 'none';
|
|
287
|
+
const suffixes = ['', '-fill', '-line', '-circle', '-extrusion'];
|
|
288
|
+
for (const suffix of suffixes) {
|
|
289
|
+
const layerId = `${uuid}${suffix}`;
|
|
290
|
+
if (this.map.getLayer(layerId)) this.map.setLayoutProperty(layerId, 'visibility', visibility);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async addControlFromModule(_src: string): Promise<void> { console.warn('Dynamic control adding not yet implemented for MapLibre'); }
|
|
295
|
+
async removeControl(_uuid: string): Promise<void> { console.warn('Control removal not yet implemented for MapLibre'); }
|
|
296
|
+
async addOverlayFromModule(_src: string, _position?: string): Promise<void> { console.warn('Dynamic overlay adding not yet implemented for MapLibre'); }
|
|
297
|
+
async removeOverlay(_uuid: string): Promise<void> { console.warn('Overlay removal not yet implemented for MapLibre'); }
|
|
298
|
+
|
|
299
|
+
async enableDrawing(geometryType: 'Point' | 'LineString' | 'Polygon', layerUuid: string): Promise<void> {
|
|
300
|
+
this.disableSelection();
|
|
301
|
+
this.disableDrawing();
|
|
302
|
+
|
|
303
|
+
this.drawMode = geometryType;
|
|
304
|
+
this.map.getCanvas().style.cursor = 'crosshair';
|
|
305
|
+
|
|
306
|
+
if (geometryType === 'Point') {
|
|
307
|
+
this.clickHandler = (e: MapMouseEvent) => {
|
|
308
|
+
const coords = [e.lngLat.lng, e.lngLat.lat];
|
|
309
|
+
this.addFeatureToLayer(layerUuid, { type: 'Point', coordinates: coords });
|
|
310
|
+
};
|
|
311
|
+
this.map.on('click', this.clickHandler);
|
|
312
|
+
} else {
|
|
313
|
+
console.warn(`${geometryType} drawing requires maplibre-gl-draw or custom implementation`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private addFeatureToLayer(layerUuid: string, geometry: any): void {
|
|
318
|
+
const source = this.map.getSource(layerUuid) as GeoJSONSource;
|
|
319
|
+
if (!source) return;
|
|
320
|
+
|
|
321
|
+
const uuid = uuidv4();
|
|
322
|
+
const gsMap = this.renderer.getGsMap();
|
|
323
|
+
const layer = gsMap.layers.find(l => l.uuid === layerUuid);
|
|
324
|
+
if (layer?.source?.type === GsSourceType.Features) {
|
|
325
|
+
const features = layer.source.features || [];
|
|
326
|
+
const coordsMercator = toWebMercator(geometry.coordinates as [number, number]);
|
|
327
|
+
|
|
328
|
+
const gsFeature = ensureUuid({
|
|
329
|
+
geometry: ensureUuid({ type: geometry.type, coordinates: coordsMercator } as GsGeometry),
|
|
330
|
+
uuid
|
|
331
|
+
} as GsFeature);
|
|
332
|
+
|
|
333
|
+
features.push(gsFeature);
|
|
334
|
+
source.setData(toGeoJsonFeatureCollection(features));
|
|
335
|
+
|
|
336
|
+
this.renderer.syncLayerFeaturesToModel(layerUuid);
|
|
337
|
+
this.renderer.triggerDirty();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async disableDrawing(): Promise<void> {
|
|
342
|
+
if (this.clickHandler) {
|
|
343
|
+
this.map.off('click', this.clickHandler);
|
|
344
|
+
this.clickHandler = undefined;
|
|
345
|
+
}
|
|
346
|
+
this.drawMode = undefined;
|
|
347
|
+
this.map.getCanvas().style.cursor = '';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async enableFeatureSelection(): Promise<void> {
|
|
351
|
+
this.disableDrawing();
|
|
352
|
+
this.disableSelection();
|
|
353
|
+
|
|
354
|
+
this.map.getCanvas().style.cursor = 'pointer';
|
|
355
|
+
|
|
356
|
+
this.clickHandler = (e: MapMouseEvent) => {
|
|
357
|
+
const features = this.map.queryRenderedFeatures(e.point);
|
|
358
|
+
|
|
359
|
+
if (features.length > 0) {
|
|
360
|
+
const feature = features[0];
|
|
361
|
+
this.selectedFeatureId = feature.id;
|
|
362
|
+
this.selectedLayerId = feature.layer.id;
|
|
363
|
+
|
|
364
|
+
const gsFeature = toGsFeature(feature as unknown as GeoJSON.Feature);
|
|
365
|
+
const layerUuid = (feature.layer as any).metadata?.[ML_KEY_GS_LAYER_UUID] ||
|
|
366
|
+
feature.layer.id.replace(/-(?:fill|line|circle|extrusion)$/, '');
|
|
367
|
+
|
|
368
|
+
this.renderer.triggerSync({ type: 'featureSelected', layerUuid, feature: gsFeature });
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
this.map.on('click', this.clickHandler);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async disableSelection(): Promise<void> {
|
|
376
|
+
if (this.clickHandler) {
|
|
377
|
+
this.map.off('click', this.clickHandler);
|
|
378
|
+
this.clickHandler = undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (this.selectedFeatureId !== undefined) {
|
|
382
|
+
this.renderer.triggerSync({ type: 'featureDeselected' });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
this.selectedFeatureId = undefined;
|
|
386
|
+
this.selectedLayerId = undefined;
|
|
387
|
+
this.map.getCanvas().style.cursor = '';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async deleteSelectedFeatures(): Promise<void> {
|
|
391
|
+
if (this.selectedFeatureId === undefined || !this.selectedLayerId) {
|
|
392
|
+
throw new Error('No features selected');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const layerUuid = this.selectedLayerId.replace(/-(?:fill|line|circle|extrusion)$/, '');
|
|
396
|
+
const source = this.map.getSource(layerUuid) as GeoJSONSource;
|
|
397
|
+
|
|
398
|
+
if (!source) throw new Error('Source not found');
|
|
399
|
+
|
|
400
|
+
const gsMap = this.renderer.getGsMap();
|
|
401
|
+
const layer = gsMap.layers.find(l => l.uuid === layerUuid);
|
|
402
|
+
|
|
403
|
+
if (layer?.source?.type === GsSourceType.Features && layer.source.features) {
|
|
404
|
+
const featureId = this.selectedFeatureId.toString();
|
|
405
|
+
layer.source.features = layer.source.features.filter(f => f.uuid !== featureId);
|
|
406
|
+
|
|
407
|
+
source.setData(toGeoJsonFeatureCollection(layer.source.features));
|
|
408
|
+
|
|
409
|
+
this.renderer.syncLayerFeaturesToModel(layerUuid);
|
|
410
|
+
this.renderer.triggerDirty();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.selectedFeatureId = undefined;
|
|
414
|
+
this.selectedLayerId = undefined;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
cleanup(): void {
|
|
418
|
+
if (this.keyHandler) {
|
|
419
|
+
document.removeEventListener('keydown', this.keyHandler);
|
|
420
|
+
this.keyHandler = undefined;
|
|
421
|
+
}
|
|
422
|
+
if (this.clickHandler) {
|
|
423
|
+
this.map.off('click', this.clickHandler);
|
|
424
|
+
this.clickHandler = undefined;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
KEY_STATE,
|
|
28
28
|
KEY_UUID,
|
|
29
29
|
KEY_URL
|
|
30
|
-
} from "
|
|
30
|
+
} from "../gs-model";
|
|
31
31
|
import {Feature, Map, Overlay, View} from "ol";
|
|
32
32
|
import {MapOptions} from "ol/Map";
|
|
33
33
|
import BaseObject from "ol/Object";
|
|
@@ -47,11 +47,11 @@ import {apply as applyMapboxStyle} from "ol-mapbox-style";
|
|
|
47
47
|
import LayerGroup from "ol/layer/Group";
|
|
48
48
|
import {Control} from "ol/control";
|
|
49
49
|
import * as ol from "./gs-olns"
|
|
50
|
-
import {lit} from "
|
|
50
|
+
import {lit} from "../gs-litns";
|
|
51
51
|
import {v4 as uuidv4} from '@kispace-io/appspace/externals/third-party'
|
|
52
52
|
import {subscribe, publish, unsubscribe} from '@kispace-io/appspace/core/events'
|
|
53
53
|
import {GsControlAdapter, GsOverlayAdapter} from "./gs-ol-adapters";
|
|
54
|
-
import {rtUtils} from "
|
|
54
|
+
import {rtUtils} from "../index";
|
|
55
55
|
import Layer from "ol/layer/Layer";
|
|
56
56
|
|
|
57
57
|
const withState = <T extends BaseObject>(state: GsState, olObject: T): T => {
|
|
@@ -597,7 +597,7 @@ export const cleanupEventSubscriptions = (olMap: Map): void => {
|
|
|
597
597
|
}
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
-
export const toOlMap = async (gsMap: GsMap, options?: MapOptions, env?: any, importer?: Importer) => {
|
|
600
|
+
export const toOlMap = async (gsMap: GsMap, options?: MapOptions, env?: any, importer?: Importer, target?: HTMLElement) => {
|
|
601
601
|
const olMap = withState(gsMap, new Map(options));
|
|
602
602
|
olMap.set(KEY_ENV, env)
|
|
603
603
|
olMap.setView(new View({
|
|
@@ -610,6 +610,12 @@ export const toOlMap = async (gsMap: GsMap, options?: MapOptions, env?: any, imp
|
|
|
610
610
|
olMap.addLayer(olLayer)
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
+
// Attach map to DOM early so it renders immediately
|
|
614
|
+
// This prevents blank screens when user modules request permissions (e.g., geolocation)
|
|
615
|
+
if (target) {
|
|
616
|
+
olMap.setTarget(target)
|
|
617
|
+
}
|
|
618
|
+
|
|
613
619
|
for (const overlay of gsMap.overlays || []) {
|
|
614
620
|
const olOverlay = toOlOverlay(overlay)
|
|
615
621
|
olMap.addOverlay(olOverlay)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {toOlMap, Importer} from "./gs-gs2ol";
|
|
2
|
-
import {GsMap} from "
|
|
2
|
+
import {GsMap} from "../gs-model";
|
|
3
3
|
import "ol/ol.css";
|
|
4
4
|
import {defaultControls, defaultInteractions} from "./gs-olns";
|
|
5
5
|
// Note: WebAwesome is imported via gs-litns (which is imported by gs-gs2ol), so it's available to user modules
|
|
6
6
|
|
|
7
|
-
export * from "
|
|
7
|
+
export * from "../gs-model";
|
|
8
8
|
export * from "./gs-gs2ol";
|
|
9
9
|
|
|
10
10
|
export interface GsAppOptions {
|
|
@@ -18,7 +18,7 @@ export interface GsAppOptions {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export const
|
|
21
|
+
export const olLib = async (options: GsAppOptions) => {
|
|
22
22
|
const mapOptions = {
|
|
23
23
|
interactions: defaultInteractions({keyboard: false}),
|
|
24
24
|
controls: defaultControls(options.mapOptions?.controls)
|
|
@@ -41,14 +41,15 @@ export const gsLib = async (options: GsAppOptions) => {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const olMap = await toOlMap(options.gsMap, mapOptions, options.env, importer)
|
|
45
|
-
|
|
46
44
|
// Handle both string selector and DOM element
|
|
47
45
|
const target = typeof options.containerSelector === 'string'
|
|
48
46
|
? document.querySelector(options.containerSelector)! as HTMLElement
|
|
49
47
|
: options.containerSelector;
|
|
50
48
|
|
|
51
|
-
|
|
49
|
+
// Create map and attach to DOM first to ensure immediate rendering
|
|
50
|
+
// Controls and overlays are loaded after the map is visible
|
|
51
|
+
const olMap = await toOlMap(options.gsMap, mapOptions, options.env, importer, target)
|
|
52
|
+
|
|
52
53
|
return olMap
|
|
53
54
|
}
|
|
54
55
|
|
package/src/ol/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// OpenLayers renderer exports
|
|
2
|
+
// Import from '@kispace-io/gs-lib/ol' for OpenLayers-specific functionality
|
|
3
|
+
|
|
4
|
+
// Core model (needed for types)
|
|
5
|
+
export * from "../gs-model"
|
|
6
|
+
|
|
7
|
+
// Renderer interface
|
|
8
|
+
export * from "../map-renderer"
|
|
9
|
+
|
|
10
|
+
// OpenLayers-specific exports
|
|
11
|
+
export * from "./gs-gs2ol"
|
|
12
|
+
export * from "./gs-ol2gs"
|
|
13
|
+
export * from "./gs-ol-adapters"
|
|
14
|
+
export * from "./gs-ol-lib"
|
|
15
|
+
|
|
16
|
+
// OpenLayers namespace
|
|
17
|
+
export * from "./gs-olns"
|
|
18
|
+
|
|
19
|
+
// OpenLayers renderer implementation
|
|
20
|
+
export { OpenLayersMapRenderer } from "./openlayers-map-renderer"
|
|
21
|
+
|