@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,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GsMap to MapLibre conversion utilities
|
|
3
|
+
* Converts the geo!space domain model to MapLibre GL structures
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Map,
|
|
8
|
+
LngLatLike,
|
|
9
|
+
StyleSpecification,
|
|
10
|
+
LayerSpecification,
|
|
11
|
+
SourceSpecification
|
|
12
|
+
} from 'maplibre-gl';
|
|
13
|
+
import { v4 as uuidv4 } from '@kispace-io/appspace/externals/third-party';
|
|
14
|
+
import { subscribe, publish } from '@kispace-io/appspace/core/events';
|
|
15
|
+
import { GsMlControl, GsMlControlAdapter, GsMlOverlayAdapter } from './gs-ml-adapters';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
GsMap,
|
|
19
|
+
GsLayer,
|
|
20
|
+
GsSource,
|
|
21
|
+
GsSourceType,
|
|
22
|
+
GsLayerType,
|
|
23
|
+
GsFeature,
|
|
24
|
+
GsStyle,
|
|
25
|
+
KEY_UUID
|
|
26
|
+
} from '../gs-model';
|
|
27
|
+
import { lit } from '../gs-litns';
|
|
28
|
+
import { rtUtils } from '../index';
|
|
29
|
+
import proj4 from 'proj4';
|
|
30
|
+
|
|
31
|
+
// Key constants for MapLibre
|
|
32
|
+
export const ML_KEY_GS_LAYER_UUID = 'gs-layer-uuid';
|
|
33
|
+
export const ML_KEY_GS_SOURCE_UUID = 'gs-source-uuid';
|
|
34
|
+
|
|
35
|
+
// Standard projection identifiers
|
|
36
|
+
export const EPSG_3857 = 'EPSG:3857'; // Web Mercator (meters)
|
|
37
|
+
export const EPSG_4326 = 'EPSG:4326'; // WGS84 (lon/lat degrees)
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert coordinates to EPSG:4326 (WGS84)
|
|
41
|
+
* MapLibre uses WGS84 (lon/lat in degrees)
|
|
42
|
+
* Uses proj4js for accurate coordinate transformation
|
|
43
|
+
*
|
|
44
|
+
* @param coords - Source coordinates
|
|
45
|
+
* @param sourceProjection - Source projection (default: EPSG:3857)
|
|
46
|
+
*/
|
|
47
|
+
export function toWgs84(coords: [number, number], sourceProjection: string = EPSG_3857): [number, number] {
|
|
48
|
+
if (sourceProjection === EPSG_4326) {
|
|
49
|
+
return coords; // Already in WGS84
|
|
50
|
+
}
|
|
51
|
+
return proj4(sourceProjection, EPSG_4326, coords) as [number, number];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert coordinates to EPSG:3857 (Web Mercator)
|
|
56
|
+
* GsMap stores coordinates in Web Mercator (meters)
|
|
57
|
+
* Uses proj4js for accurate coordinate transformation
|
|
58
|
+
*
|
|
59
|
+
* @param coords - Source coordinates
|
|
60
|
+
* @param sourceProjection - Source projection (default: EPSG:4326)
|
|
61
|
+
*/
|
|
62
|
+
export function toWebMercator(coords: [number, number], sourceProjection: string = EPSG_4326): [number, number] {
|
|
63
|
+
if (sourceProjection === EPSG_3857) {
|
|
64
|
+
return coords; // Already in Web Mercator
|
|
65
|
+
}
|
|
66
|
+
return proj4(sourceProjection, EPSG_3857, coords) as [number, number];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generic coordinate transformation between any two projections
|
|
71
|
+
* Uses proj4js for accurate coordinate transformation
|
|
72
|
+
*
|
|
73
|
+
* @param coords - Source coordinates
|
|
74
|
+
* @param fromProjection - Source projection
|
|
75
|
+
* @param toProjection - Target projection
|
|
76
|
+
*/
|
|
77
|
+
export function transformCoords(
|
|
78
|
+
coords: [number, number],
|
|
79
|
+
fromProjection: string,
|
|
80
|
+
toProjection: string
|
|
81
|
+
): [number, number] {
|
|
82
|
+
if (fromProjection === toProjection) {
|
|
83
|
+
return coords;
|
|
84
|
+
}
|
|
85
|
+
return proj4(fromProjection, toProjection, coords) as [number, number];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Convert coordinates to WGS84 for any geometry type
|
|
90
|
+
*
|
|
91
|
+
* @param coords - Geometry coordinates
|
|
92
|
+
* @param geometryType - GeoJSON geometry type
|
|
93
|
+
* @param sourceProjection - Source projection (default: EPSG:3857)
|
|
94
|
+
*/
|
|
95
|
+
function convertCoordinatesToWgs84(coords: any, geometryType: string, sourceProjection: string = EPSG_3857): any {
|
|
96
|
+
switch (geometryType) {
|
|
97
|
+
case 'Point':
|
|
98
|
+
return toWgs84(coords as [number, number], sourceProjection);
|
|
99
|
+
case 'LineString':
|
|
100
|
+
case 'MultiPoint':
|
|
101
|
+
return (coords as [number, number][]).map(c => toWgs84(c, sourceProjection));
|
|
102
|
+
case 'Polygon':
|
|
103
|
+
case 'MultiLineString':
|
|
104
|
+
return (coords as [number, number][][]).map(ring => ring.map(c => toWgs84(c, sourceProjection)));
|
|
105
|
+
case 'MultiPolygon':
|
|
106
|
+
return (coords as [number, number][][][]).map(poly => poly.map(ring => ring.map(c => toWgs84(c, sourceProjection))));
|
|
107
|
+
default:
|
|
108
|
+
return coords;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Convert GsFeature to GeoJSON Feature (with coordinate conversion to WGS84)
|
|
114
|
+
*/
|
|
115
|
+
/**
|
|
116
|
+
* Convert GsFeature to GeoJSON Feature (with coordinate conversion to WGS84)
|
|
117
|
+
*
|
|
118
|
+
* @param feature - GsFeature to convert
|
|
119
|
+
* @param sourceProjection - Source projection of coordinates (default: EPSG:3857)
|
|
120
|
+
*/
|
|
121
|
+
export function toGeoJsonFeature(feature: GsFeature, sourceProjection: string = EPSG_3857): GeoJSON.Feature {
|
|
122
|
+
const convertedCoords = convertCoordinatesToWgs84(
|
|
123
|
+
feature.geometry.coordinates,
|
|
124
|
+
feature.geometry.type,
|
|
125
|
+
sourceProjection
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
type: 'Feature',
|
|
130
|
+
id: feature.uuid,
|
|
131
|
+
geometry: {
|
|
132
|
+
type: feature.geometry.type,
|
|
133
|
+
coordinates: convertedCoords
|
|
134
|
+
} as GeoJSON.Geometry,
|
|
135
|
+
properties: {
|
|
136
|
+
...feature.state,
|
|
137
|
+
[KEY_UUID]: feature.uuid
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Convert array of GsFeatures to GeoJSON FeatureCollection
|
|
144
|
+
*/
|
|
145
|
+
/**
|
|
146
|
+
* Convert array of GsFeatures to GeoJSON FeatureCollection
|
|
147
|
+
*
|
|
148
|
+
* @param features - Array of GsFeatures
|
|
149
|
+
* @param sourceProjection - Source projection of coordinates (default: EPSG:3857)
|
|
150
|
+
*/
|
|
151
|
+
export function toGeoJsonFeatureCollection(features: GsFeature[], sourceProjection: string = EPSG_3857): GeoJSON.FeatureCollection {
|
|
152
|
+
return {
|
|
153
|
+
type: 'FeatureCollection',
|
|
154
|
+
features: features.map(f => toGeoJsonFeature(f, sourceProjection))
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build WMS tile URL from GsSource
|
|
160
|
+
*/
|
|
161
|
+
function buildWmsTileUrl(source: GsSource): string {
|
|
162
|
+
const baseUrl = source.url!;
|
|
163
|
+
const params = new URLSearchParams({
|
|
164
|
+
SERVICE: 'WMS',
|
|
165
|
+
VERSION: '1.1.1',
|
|
166
|
+
REQUEST: 'GetMap',
|
|
167
|
+
FORMAT: 'image/png',
|
|
168
|
+
TRANSPARENT: 'true',
|
|
169
|
+
SRS: 'EPSG:3857',
|
|
170
|
+
WIDTH: '256',
|
|
171
|
+
HEIGHT: '256',
|
|
172
|
+
BBOX: '{bbox-epsg-3857}',
|
|
173
|
+
...source.state
|
|
174
|
+
});
|
|
175
|
+
const separator = baseUrl.includes('?') ? '&' : '?';
|
|
176
|
+
return `${baseUrl}${separator}${params.toString()}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Convert GsSource to MapLibre SourceSpecification
|
|
181
|
+
*/
|
|
182
|
+
export function toMlSource(source: GsSource, _layer?: GsLayer): SourceSpecification | null {
|
|
183
|
+
switch (source.type) {
|
|
184
|
+
case GsSourceType.OSM:
|
|
185
|
+
return {
|
|
186
|
+
type: 'raster',
|
|
187
|
+
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
|
188
|
+
tileSize: 256,
|
|
189
|
+
attribution: '© OpenStreetMap contributors'
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
case GsSourceType.XYZ:
|
|
193
|
+
return {
|
|
194
|
+
type: 'raster',
|
|
195
|
+
tiles: [source.url!],
|
|
196
|
+
tileSize: 256
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
case GsSourceType.GeoJSON:
|
|
200
|
+
return {
|
|
201
|
+
type: 'geojson',
|
|
202
|
+
data: source.url!
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
case GsSourceType.Features:
|
|
206
|
+
return {
|
|
207
|
+
type: 'geojson',
|
|
208
|
+
data: toGeoJsonFeatureCollection(source.features || [])
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
case GsSourceType.WMS:
|
|
212
|
+
return {
|
|
213
|
+
type: 'raster',
|
|
214
|
+
tiles: [buildWmsTileUrl(source)],
|
|
215
|
+
tileSize: 256
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
case GsSourceType.WMTS:
|
|
219
|
+
// WMTS needs tile URL construction - simplified for now
|
|
220
|
+
return {
|
|
221
|
+
type: 'raster',
|
|
222
|
+
tiles: [source.url!],
|
|
223
|
+
tileSize: 256
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
case GsSourceType.BM:
|
|
227
|
+
// Basemap.de style - handled separately as full style
|
|
228
|
+
return null;
|
|
229
|
+
|
|
230
|
+
case GsSourceType.GeoTIFF:
|
|
231
|
+
// GeoTIFF requires special handling with maplibre-gl-cog
|
|
232
|
+
console.warn('GeoTIFF source type not yet supported in MapLibre renderer');
|
|
233
|
+
return null;
|
|
234
|
+
|
|
235
|
+
case GsSourceType.GPX:
|
|
236
|
+
case GsSourceType.KML:
|
|
237
|
+
// These need format conversion - would require togeojson library
|
|
238
|
+
console.warn(`${source.type} source type requires conversion to GeoJSON`);
|
|
239
|
+
return null;
|
|
240
|
+
|
|
241
|
+
default:
|
|
242
|
+
console.warn(`Unknown source type: ${source.type}`);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Convert GsStyle to MapLibre paint properties for fill layers
|
|
249
|
+
*/
|
|
250
|
+
export function toMlFillPaint(style?: GsStyle): Record<string, any> {
|
|
251
|
+
const paint: Record<string, any> = {};
|
|
252
|
+
|
|
253
|
+
if (style?.fill?.color) {
|
|
254
|
+
paint['fill-color'] = style.fill.color;
|
|
255
|
+
} else {
|
|
256
|
+
paint['fill-color'] = 'rgba(0, 100, 255, 0.3)';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (style?.stroke?.color) {
|
|
260
|
+
paint['fill-outline-color'] = style.stroke.color;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return paint;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Convert GsStyle to MapLibre paint properties for line layers
|
|
268
|
+
*/
|
|
269
|
+
export function toMlLinePaint(style?: GsStyle): Record<string, any> {
|
|
270
|
+
const paint: Record<string, any> = {};
|
|
271
|
+
|
|
272
|
+
if (style?.stroke?.color) {
|
|
273
|
+
paint['line-color'] = style.stroke.color;
|
|
274
|
+
} else {
|
|
275
|
+
paint['line-color'] = 'rgba(0, 100, 255, 0.8)';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (style?.stroke?.width) {
|
|
279
|
+
paint['line-width'] = style.stroke.width;
|
|
280
|
+
} else {
|
|
281
|
+
paint['line-width'] = 2;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (style?.stroke?.lineDash) {
|
|
285
|
+
paint['line-dasharray'] = style.stroke.lineDash;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return paint;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Convert GsStyle to MapLibre paint properties for circle layers
|
|
293
|
+
*/
|
|
294
|
+
export function toMlCirclePaint(style?: GsStyle): Record<string, any> {
|
|
295
|
+
const paint: Record<string, any> = {};
|
|
296
|
+
|
|
297
|
+
const image = style?.image;
|
|
298
|
+
if (image?.type === 'circle') {
|
|
299
|
+
paint['circle-radius'] = image.radius || 5;
|
|
300
|
+
|
|
301
|
+
if (image.fill?.color) {
|
|
302
|
+
paint['circle-color'] = image.fill.color;
|
|
303
|
+
} else {
|
|
304
|
+
paint['circle-color'] = 'rgba(0, 100, 255, 0.8)';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (image.stroke?.color) {
|
|
308
|
+
paint['circle-stroke-color'] = image.stroke.color;
|
|
309
|
+
} else {
|
|
310
|
+
paint['circle-stroke-color'] = 'white';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (image.stroke?.width) {
|
|
314
|
+
paint['circle-stroke-width'] = image.stroke.width;
|
|
315
|
+
} else {
|
|
316
|
+
paint['circle-stroke-width'] = 2;
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
// Default circle style
|
|
320
|
+
paint['circle-radius'] = 5;
|
|
321
|
+
paint['circle-color'] = 'rgba(0, 100, 255, 0.8)';
|
|
322
|
+
paint['circle-stroke-color'] = 'white';
|
|
323
|
+
paint['circle-stroke-width'] = 2;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return paint;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Convert GsStyle to MapLibre paint properties for fill-extrusion layers (3D buildings)
|
|
331
|
+
* Uses data-driven styling to get height and color from feature properties
|
|
332
|
+
*/
|
|
333
|
+
export function toMlFillExtrusionPaint(style?: GsStyle): Record<string, any> {
|
|
334
|
+
const paint: Record<string, any> = {};
|
|
335
|
+
|
|
336
|
+
// Use data-driven color from feature properties, or fall back to style/default
|
|
337
|
+
paint['fill-extrusion-color'] = [
|
|
338
|
+
'case',
|
|
339
|
+
['has', 'color'],
|
|
340
|
+
['get', 'color'],
|
|
341
|
+
style?.fill?.color || 'rgba(74, 144, 217, 0.8)'
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
// Get height from feature properties, default to 10 meters
|
|
345
|
+
paint['fill-extrusion-height'] = [
|
|
346
|
+
'case',
|
|
347
|
+
['has', 'height'],
|
|
348
|
+
['get', 'height'],
|
|
349
|
+
10
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
// Base height (for floating buildings), default to 0
|
|
353
|
+
paint['fill-extrusion-base'] = [
|
|
354
|
+
'case',
|
|
355
|
+
['has', 'base_height'],
|
|
356
|
+
['get', 'base_height'],
|
|
357
|
+
0
|
|
358
|
+
];
|
|
359
|
+
|
|
360
|
+
// Opacity
|
|
361
|
+
paint['fill-extrusion-opacity'] = 0.85;
|
|
362
|
+
|
|
363
|
+
return paint;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Convert GsLayer to MapLibre LayerSpecification(s)
|
|
368
|
+
* Returns an array because vector layers may need multiple MapLibre layers
|
|
369
|
+
* (one for each geometry type: fill, line, circle, fill-extrusion)
|
|
370
|
+
*/
|
|
371
|
+
export function toMlLayers(layer: GsLayer, sourceId: string, defaultStyle?: GsStyle): LayerSpecification[] {
|
|
372
|
+
const layers: LayerSpecification[] = [];
|
|
373
|
+
const baseId = layer.uuid || sourceId;
|
|
374
|
+
|
|
375
|
+
if (layer.type === GsLayerType.TILE) {
|
|
376
|
+
layers.push({
|
|
377
|
+
id: baseId,
|
|
378
|
+
type: 'raster',
|
|
379
|
+
source: sourceId,
|
|
380
|
+
layout: {
|
|
381
|
+
visibility: layer.visible !== false ? 'visible' : 'none'
|
|
382
|
+
}
|
|
383
|
+
} as LayerSpecification);
|
|
384
|
+
} else if (layer.type === GsLayerType.VECTOR) {
|
|
385
|
+
// Add fill layer for polygons WITHOUT height property (flat 2D polygons)
|
|
386
|
+
layers.push({
|
|
387
|
+
id: `${baseId}-fill`,
|
|
388
|
+
type: 'fill',
|
|
389
|
+
source: sourceId,
|
|
390
|
+
filter: ['all',
|
|
391
|
+
['any',
|
|
392
|
+
['==', ['geometry-type'], 'Polygon'],
|
|
393
|
+
['==', ['geometry-type'], 'MultiPolygon']
|
|
394
|
+
],
|
|
395
|
+
['!', ['has', 'height']]
|
|
396
|
+
],
|
|
397
|
+
paint: toMlFillPaint(defaultStyle),
|
|
398
|
+
layout: {
|
|
399
|
+
visibility: layer.visible !== false ? 'visible' : 'none'
|
|
400
|
+
},
|
|
401
|
+
metadata: { [ML_KEY_GS_LAYER_UUID]: layer.uuid }
|
|
402
|
+
} as LayerSpecification);
|
|
403
|
+
|
|
404
|
+
// Add fill-extrusion layer for polygons WITH height property (3D buildings)
|
|
405
|
+
layers.push({
|
|
406
|
+
id: `${baseId}-extrusion`,
|
|
407
|
+
type: 'fill-extrusion',
|
|
408
|
+
source: sourceId,
|
|
409
|
+
filter: ['all',
|
|
410
|
+
['any',
|
|
411
|
+
['==', ['geometry-type'], 'Polygon'],
|
|
412
|
+
['==', ['geometry-type'], 'MultiPolygon']
|
|
413
|
+
],
|
|
414
|
+
['has', 'height']
|
|
415
|
+
],
|
|
416
|
+
paint: toMlFillExtrusionPaint(defaultStyle),
|
|
417
|
+
layout: {
|
|
418
|
+
visibility: layer.visible !== false ? 'visible' : 'none'
|
|
419
|
+
},
|
|
420
|
+
metadata: { [ML_KEY_GS_LAYER_UUID]: layer.uuid }
|
|
421
|
+
} as LayerSpecification);
|
|
422
|
+
|
|
423
|
+
// Add line layer for linestrings
|
|
424
|
+
layers.push({
|
|
425
|
+
id: `${baseId}-line`,
|
|
426
|
+
type: 'line',
|
|
427
|
+
source: sourceId,
|
|
428
|
+
filter: ['any',
|
|
429
|
+
['==', ['geometry-type'], 'LineString'],
|
|
430
|
+
['==', ['geometry-type'], 'MultiLineString']
|
|
431
|
+
],
|
|
432
|
+
paint: toMlLinePaint(defaultStyle),
|
|
433
|
+
layout: {
|
|
434
|
+
visibility: layer.visible !== false ? 'visible' : 'none'
|
|
435
|
+
},
|
|
436
|
+
metadata: { [ML_KEY_GS_LAYER_UUID]: layer.uuid }
|
|
437
|
+
} as LayerSpecification);
|
|
438
|
+
|
|
439
|
+
// Add circle layer for points
|
|
440
|
+
layers.push({
|
|
441
|
+
id: `${baseId}-circle`,
|
|
442
|
+
type: 'circle',
|
|
443
|
+
source: sourceId,
|
|
444
|
+
filter: ['any',
|
|
445
|
+
['==', ['geometry-type'], 'Point'],
|
|
446
|
+
['==', ['geometry-type'], 'MultiPoint']
|
|
447
|
+
],
|
|
448
|
+
paint: toMlCirclePaint(defaultStyle),
|
|
449
|
+
layout: {
|
|
450
|
+
visibility: layer.visible !== false ? 'visible' : 'none'
|
|
451
|
+
},
|
|
452
|
+
metadata: { [ML_KEY_GS_LAYER_UUID]: layer.uuid }
|
|
453
|
+
} as LayerSpecification);
|
|
454
|
+
} else if (layer.type === GsLayerType.GROUP) {
|
|
455
|
+
// GROUP layers with BM source use external style URL
|
|
456
|
+
// This is handled separately by loading the style
|
|
457
|
+
console.log('GROUP layer detected - style URL:', layer.source.url);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return layers;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Build initial MapLibre style from GsMap
|
|
465
|
+
*/
|
|
466
|
+
export function buildInitialStyle(gsMap: GsMap): StyleSpecification {
|
|
467
|
+
const sources: Record<string, SourceSpecification> = {};
|
|
468
|
+
const layers: LayerSpecification[] = [];
|
|
469
|
+
|
|
470
|
+
// Get default style for features (using point style as default for all geometry types)
|
|
471
|
+
const defaultStyle = gsMap.styles?.['default-point'];
|
|
472
|
+
|
|
473
|
+
// Process each layer
|
|
474
|
+
gsMap.layers.forEach((layer, index) => {
|
|
475
|
+
// Skip GROUP layers - they load external styles
|
|
476
|
+
if (layer.type === GsLayerType.GROUP && layer.source.type === GsSourceType.BM) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const sourceId = layer.uuid || `layer-${index}`;
|
|
481
|
+
const source = toMlSource(layer.source, layer);
|
|
482
|
+
|
|
483
|
+
if (source) {
|
|
484
|
+
sources[sourceId] = source;
|
|
485
|
+
const mlLayers = toMlLayers(layer, sourceId, defaultStyle);
|
|
486
|
+
layers.push(...mlLayers);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
version: 8,
|
|
492
|
+
sources,
|
|
493
|
+
layers,
|
|
494
|
+
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf'
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
export type MlImporter = (src: string) => Promise<any>;
|
|
500
|
+
export const DefaultMlImporter: MlImporter = (src: string) => import(src);
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Import and initialize a control module for MapLibre
|
|
504
|
+
*/
|
|
505
|
+
export async function importMlControlSource(
|
|
506
|
+
map: Map,
|
|
507
|
+
control: GsMlControl,
|
|
508
|
+
src: string,
|
|
509
|
+
env?: any,
|
|
510
|
+
importer?: MlImporter
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
const adapter = control.getAdapter();
|
|
513
|
+
const ml = await import('./gs-mlns');
|
|
514
|
+
|
|
515
|
+
return (importer || DefaultMlImporter)(src).then((mod) => {
|
|
516
|
+
const init = () => {
|
|
517
|
+
const vars: any = {
|
|
518
|
+
...lit,
|
|
519
|
+
lit: lit,
|
|
520
|
+
style: adapter.style.bind(adapter),
|
|
521
|
+
render: adapter.render.bind(adapter),
|
|
522
|
+
map: map,
|
|
523
|
+
element: adapter.getElement(),
|
|
524
|
+
querySelector: adapter.getElement().querySelector.bind(adapter.getElement()),
|
|
525
|
+
querySelectorAll: adapter.getElement().querySelectorAll.bind(adapter.getElement()),
|
|
526
|
+
ml: ml,
|
|
527
|
+
env: env || {},
|
|
528
|
+
utils: {
|
|
529
|
+
uuid: uuidv4
|
|
530
|
+
},
|
|
531
|
+
asset: (path: string) => {
|
|
532
|
+
return rtUtils.resolveUrl(`assets/${path}`);
|
|
533
|
+
},
|
|
534
|
+
signal: (_name: string) => {},
|
|
535
|
+
events: (topic: string, callback: Function | any) => {
|
|
536
|
+
if (callback instanceof Function) {
|
|
537
|
+
const token = subscribe(topic, callback);
|
|
538
|
+
return token;
|
|
539
|
+
} else {
|
|
540
|
+
return publish(topic, callback);
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
settings: (key: string, callback?: Function | any) => {
|
|
544
|
+
const storageKey = 'gs-settings';
|
|
545
|
+
|
|
546
|
+
const loadSettings = (): any => {
|
|
547
|
+
try {
|
|
548
|
+
const stored = localStorage.getItem(storageKey);
|
|
549
|
+
return stored ? JSON.parse(stored) : {};
|
|
550
|
+
} catch {
|
|
551
|
+
return {};
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const saveSettings = (settings: any): void => {
|
|
556
|
+
try {
|
|
557
|
+
localStorage.setItem(storageKey, JSON.stringify(settings));
|
|
558
|
+
} catch (error) {
|
|
559
|
+
console.error('Failed to save settings:', error);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const settings = loadSettings();
|
|
564
|
+
|
|
565
|
+
if (callback === undefined) {
|
|
566
|
+
return settings[key];
|
|
567
|
+
}
|
|
568
|
+
if (callback instanceof Function) {
|
|
569
|
+
vars.events(key, callback);
|
|
570
|
+
callback(settings[key]);
|
|
571
|
+
return settings[key];
|
|
572
|
+
}
|
|
573
|
+
settings[key] = callback;
|
|
574
|
+
saveSettings(settings);
|
|
575
|
+
return publish(key, callback);
|
|
576
|
+
},
|
|
577
|
+
control: adapter,
|
|
578
|
+
state: <T>(initialValue: T) => {
|
|
579
|
+
if (typeof initialValue === 'object' && initialValue !== null && !Array.isArray(initialValue)) {
|
|
580
|
+
const values: any = { ...initialValue };
|
|
581
|
+
return new Proxy({} as any, {
|
|
582
|
+
get(_target, prop: string) {
|
|
583
|
+
return values[prop];
|
|
584
|
+
},
|
|
585
|
+
set(_target, prop: string, newValue: any) {
|
|
586
|
+
if (values[prop] !== newValue) {
|
|
587
|
+
values[prop] = newValue;
|
|
588
|
+
adapter.render();
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
} else {
|
|
594
|
+
let value = initialValue;
|
|
595
|
+
return {
|
|
596
|
+
get value() { return value; },
|
|
597
|
+
set value(newValue: any) {
|
|
598
|
+
if (value !== newValue) {
|
|
599
|
+
value = newValue;
|
|
600
|
+
adapter.render();
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const templateFunction = mod instanceof Function ? mod : mod.default;
|
|
609
|
+
if (templateFunction) {
|
|
610
|
+
const component = templateFunction(vars);
|
|
611
|
+
if (component instanceof Function) {
|
|
612
|
+
adapter.render(component);
|
|
613
|
+
} else {
|
|
614
|
+
adapter.render(component);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// Initialize after map is loaded
|
|
620
|
+
if (map.loaded()) {
|
|
621
|
+
init();
|
|
622
|
+
} else {
|
|
623
|
+
map.on('load', init);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Import and initialize an overlay module for MapLibre
|
|
630
|
+
*/
|
|
631
|
+
export async function importMlOverlaySource(
|
|
632
|
+
map: Map,
|
|
633
|
+
adapter: GsMlOverlayAdapter,
|
|
634
|
+
src: string,
|
|
635
|
+
env?: any,
|
|
636
|
+
importer?: MlImporter
|
|
637
|
+
): Promise<void> {
|
|
638
|
+
const ml = await import('./gs-mlns');
|
|
639
|
+
|
|
640
|
+
return (importer || DefaultMlImporter)(src).then((mod) => {
|
|
641
|
+
const init = () => {
|
|
642
|
+
const vars: any = {
|
|
643
|
+
...lit,
|
|
644
|
+
lit: lit,
|
|
645
|
+
style: adapter.style.bind(adapter),
|
|
646
|
+
render: adapter.render.bind(adapter),
|
|
647
|
+
map: map,
|
|
648
|
+
element: adapter.getElement(),
|
|
649
|
+
querySelector: adapter.getElement().querySelector.bind(adapter.getElement()),
|
|
650
|
+
querySelectorAll: adapter.getElement().querySelectorAll.bind(adapter.getElement()),
|
|
651
|
+
ml: ml,
|
|
652
|
+
env: env || {},
|
|
653
|
+
utils: {
|
|
654
|
+
uuid: uuidv4
|
|
655
|
+
},
|
|
656
|
+
asset: (path: string) => {
|
|
657
|
+
return rtUtils.resolveUrl(`assets/${path}`);
|
|
658
|
+
},
|
|
659
|
+
signal: (_name: string) => {},
|
|
660
|
+
events: (topic: string, callback: Function | any) => {
|
|
661
|
+
if (callback instanceof Function) {
|
|
662
|
+
return subscribe(topic, callback);
|
|
663
|
+
} else {
|
|
664
|
+
return publish(topic, callback);
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
overlay: adapter,
|
|
668
|
+
state: <T>(initialValue: T) => {
|
|
669
|
+
if (typeof initialValue === 'object' && initialValue !== null && !Array.isArray(initialValue)) {
|
|
670
|
+
const values: any = { ...initialValue };
|
|
671
|
+
return new Proxy({} as any, {
|
|
672
|
+
get(_target, prop: string) {
|
|
673
|
+
return values[prop];
|
|
674
|
+
},
|
|
675
|
+
set(_target, prop: string, newValue: any) {
|
|
676
|
+
if (values[prop] !== newValue) {
|
|
677
|
+
values[prop] = newValue;
|
|
678
|
+
adapter.render();
|
|
679
|
+
}
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
} else {
|
|
684
|
+
let value = initialValue;
|
|
685
|
+
return {
|
|
686
|
+
get value() { return value; },
|
|
687
|
+
set value(newValue: any) {
|
|
688
|
+
if (value !== newValue) {
|
|
689
|
+
value = newValue;
|
|
690
|
+
adapter.render();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
const templateFunction = mod instanceof Function ? mod : mod.default;
|
|
699
|
+
if (templateFunction) {
|
|
700
|
+
const component = templateFunction(vars);
|
|
701
|
+
if (component instanceof Function) {
|
|
702
|
+
adapter.render(component);
|
|
703
|
+
} else {
|
|
704
|
+
adapter.render(component);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
if (map.loaded()) {
|
|
710
|
+
init();
|
|
711
|
+
} else {
|
|
712
|
+
map.on('load', init);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
|