aur-openlayers 19.6.10 → 19.6.12
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/fesm2022/aur-openlayers.mjs +231 -0
- package/fesm2022/aur-openlayers.mjs.map +1 -1
- package/lib/map-framework/public/types.d.ts +58 -0
- package/lib/map-framework/runtime/decorations/start-finish-decoration-manager.d.ts +28 -0
- package/lib/map-framework/runtime/export/composite-viewport.d.ts +16 -0
- package/lib/map-framework/runtime/export/export-image.d.ts +15 -0
- package/package.json +1 -1
|
@@ -211,6 +211,114 @@ function collectLayersExtent(layers, layerIds) {
|
|
|
211
211
|
return isEmpty(extent) ? null : extent;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Композитит canvas'ы ОДНОГО viewport карты в офскрин-canvas заданного размера.
|
|
216
|
+
*
|
|
217
|
+
* Берёт только `.ol-layer canvas` внутри переданного `viewport` (а не из всего
|
|
218
|
+
* документа), применяя per-canvas `opacity` родителя и матрицу `style.transform`
|
|
219
|
+
* по образцу официального примера экспорта OpenLayers. Размер результата всегда
|
|
220
|
+
* равен `[width, height]`; high-DPR backing слоёв ужимается в него через матрицу.
|
|
221
|
+
*/
|
|
222
|
+
function compositeViewport(viewport, [width, height], opts = {}) {
|
|
223
|
+
const out = document.createElement('canvas');
|
|
224
|
+
out.width = width;
|
|
225
|
+
out.height = height;
|
|
226
|
+
const ctx = out.getContext('2d');
|
|
227
|
+
if (opts.background) {
|
|
228
|
+
ctx.fillStyle = opts.background;
|
|
229
|
+
ctx.fillRect(0, 0, width, height);
|
|
230
|
+
}
|
|
231
|
+
const canvases = viewport.querySelectorAll('.ol-layer canvas, canvas.ol-layer');
|
|
232
|
+
canvases.forEach((canvas) => {
|
|
233
|
+
if (canvas.width === 0 || canvas.height === 0)
|
|
234
|
+
return;
|
|
235
|
+
const parent = canvas.parentNode;
|
|
236
|
+
const opacity = parent?.style.opacity || canvas.style.opacity;
|
|
237
|
+
ctx.globalAlpha = opacity === '' ? 1 : Number(opacity);
|
|
238
|
+
const transform = canvas.style.transform;
|
|
239
|
+
const match = transform ? transform.match(/^matrix\(([^)]+)\)$/) : null;
|
|
240
|
+
const matrix = match
|
|
241
|
+
? match[1].split(',').map(Number)
|
|
242
|
+
: [
|
|
243
|
+
(parseFloat(canvas.style.width) || canvas.width) / canvas.width,
|
|
244
|
+
0,
|
|
245
|
+
0,
|
|
246
|
+
(parseFloat(canvas.style.height) || canvas.height) / canvas.height,
|
|
247
|
+
0,
|
|
248
|
+
0,
|
|
249
|
+
];
|
|
250
|
+
ctx.setTransform(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);
|
|
251
|
+
const bgColor = parent?.style.backgroundColor;
|
|
252
|
+
if (bgColor) {
|
|
253
|
+
ctx.fillStyle = bgColor;
|
|
254
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
255
|
+
}
|
|
256
|
+
ctx.drawImage(canvas, 0, 0);
|
|
257
|
+
});
|
|
258
|
+
ctx.globalAlpha = 1;
|
|
259
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Создаёт функцию `exportImage` для конкретной карты.
|
|
265
|
+
*
|
|
266
|
+
* Параллельные вызовы сериализуются (общий мутируемый `map`): каждый следующий
|
|
267
|
+
* экспорт стартует только после завершения предыдущего. Цикл одного экспорта:
|
|
268
|
+
* snapshot → setSize → fit → ожидание `rendercomplete` → композит → restore.
|
|
269
|
+
*
|
|
270
|
+
* Инвариант: подписка `map.once('rendercomplete')` и последующие
|
|
271
|
+
* `setSize`/`fit`/`render` выполняются синхронно (никаких `await` до подписки),
|
|
272
|
+
* иначе следующий экспорт мог бы поймать `rendercomplete` от restore-рендера
|
|
273
|
+
* предыдущего.
|
|
274
|
+
*/
|
|
275
|
+
function createExportImage(map) {
|
|
276
|
+
let tail = Promise.resolve();
|
|
277
|
+
const doExport = async (options) => {
|
|
278
|
+
const view = map.getView();
|
|
279
|
+
const size0 = map.getSize();
|
|
280
|
+
if (!size0) {
|
|
281
|
+
throw new Error('exportImage: map has no size yet (is it added to the DOM?)');
|
|
282
|
+
}
|
|
283
|
+
const resolution0 = view.getResolution();
|
|
284
|
+
const center0 = view.getCenter();
|
|
285
|
+
const extent0 = view.calculateExtent(size0);
|
|
286
|
+
const { size, format = 'image/png', quality = 0.92, background, fit = 'currentExtent', } = options;
|
|
287
|
+
try {
|
|
288
|
+
const rendered = new Promise((resolve) => {
|
|
289
|
+
map.once('rendercomplete', () => resolve());
|
|
290
|
+
});
|
|
291
|
+
map.setSize(size);
|
|
292
|
+
if (fit === 'currentExtent') {
|
|
293
|
+
view.fit(extent0, { size });
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
if (resolution0 != null)
|
|
297
|
+
view.setResolution(resolution0);
|
|
298
|
+
if (center0)
|
|
299
|
+
view.setCenter(center0);
|
|
300
|
+
}
|
|
301
|
+
map.render(); // гарантия кадра даже при size === size0
|
|
302
|
+
await rendered;
|
|
303
|
+
const canvas = compositeViewport(map.getViewport(), size, { background });
|
|
304
|
+
return canvas.toDataURL(format, quality);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
map.setSize(size0);
|
|
308
|
+
if (resolution0 != null)
|
|
309
|
+
view.setResolution(resolution0);
|
|
310
|
+
if (center0)
|
|
311
|
+
view.setCenter(center0);
|
|
312
|
+
map.render();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
return (options) => {
|
|
316
|
+
const run = tail.then(() => doExport(options));
|
|
317
|
+
tail = run.catch(() => { }); // очередь переживает ошибку одного экспорта
|
|
318
|
+
return run;
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
214
322
|
const createMapContext = (map, layers, popupHost, scheduler = new FlushScheduler()) => {
|
|
215
323
|
return {
|
|
216
324
|
map,
|
|
@@ -227,6 +335,7 @@ const createMapContext = (map, layers, popupHost, scheduler = new FlushScheduler
|
|
|
227
335
|
if (extent)
|
|
228
336
|
map.getView().fit(extent, toOlFitOptions(opts, map));
|
|
229
337
|
},
|
|
338
|
+
exportImage: createExportImage(map),
|
|
230
339
|
};
|
|
231
340
|
};
|
|
232
341
|
|
|
@@ -2665,6 +2774,117 @@ class BufferDecorationManager {
|
|
|
2665
2774
|
}
|
|
2666
2775
|
}
|
|
2667
2776
|
|
|
2777
|
+
/** Creates a point Feature at `coord` with the given style and id. */
|
|
2778
|
+
function makePoint(coord, style, id) {
|
|
2779
|
+
const f = new Feature({ geometry: new Point(coord) });
|
|
2780
|
+
f.setId(id);
|
|
2781
|
+
f.setStyle(Array.isArray(style) ? style : [style]);
|
|
2782
|
+
return f;
|
|
2783
|
+
}
|
|
2784
|
+
class StartFinishDecorationManager {
|
|
2785
|
+
source = new VectorSource();
|
|
2786
|
+
layer;
|
|
2787
|
+
config;
|
|
2788
|
+
map;
|
|
2789
|
+
parentLayer;
|
|
2790
|
+
parentApi;
|
|
2791
|
+
moveEndKey;
|
|
2792
|
+
visibilityKey;
|
|
2793
|
+
unsubCollection;
|
|
2794
|
+
unsubChanges;
|
|
2795
|
+
rafId = null;
|
|
2796
|
+
constructor(options) {
|
|
2797
|
+
this.config = options.config;
|
|
2798
|
+
this.map = options.map;
|
|
2799
|
+
this.parentLayer = options.parentLayer;
|
|
2800
|
+
this.parentApi = options.parentApi;
|
|
2801
|
+
const parentZ = this.parentLayer.getZIndex() ?? 0; // ?? 0 mirrors BufferDecorationManager: layers without explicit zIndex default to 0, so decoration sits at parentZ + 3
|
|
2802
|
+
this.layer = new VectorLayer({
|
|
2803
|
+
source: this.source,
|
|
2804
|
+
zIndex: parentZ + 3,
|
|
2805
|
+
});
|
|
2806
|
+
this.layer.set('id', '__decoration_start_finish');
|
|
2807
|
+
this.map.addLayer(this.layer);
|
|
2808
|
+
this.syncVisibility();
|
|
2809
|
+
this.syncOpacity();
|
|
2810
|
+
this.visibilityKey = this.parentLayer.on('change:visible', () => {
|
|
2811
|
+
this.syncVisibility();
|
|
2812
|
+
if (this.parentLayer.getVisible()) {
|
|
2813
|
+
this.scheduleUpdate();
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
this.moveEndKey = this.map.on('moveend', () => this.scheduleUpdate());
|
|
2817
|
+
this.unsubCollection = this.parentApi.onModelsCollectionChanged(() => this.scheduleUpdate());
|
|
2818
|
+
this.unsubChanges = this.parentApi.onModelsChanged?.(() => this.scheduleUpdate());
|
|
2819
|
+
}
|
|
2820
|
+
scheduleUpdate() {
|
|
2821
|
+
if (this.rafId !== null)
|
|
2822
|
+
return;
|
|
2823
|
+
this.rafId = requestAnimationFrame(() => {
|
|
2824
|
+
this.rafId = null;
|
|
2825
|
+
this.rebuild();
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
rebuild() {
|
|
2829
|
+
this.syncVisibility();
|
|
2830
|
+
this.syncOpacity();
|
|
2831
|
+
if (!this.parentLayer.getVisible()) {
|
|
2832
|
+
this.source.clear();
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
const parentSource = this.parentLayer.getSource();
|
|
2836
|
+
if (!parentSource) {
|
|
2837
|
+
this.source.clear();
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
const { start, finish, collapsed } = this.config;
|
|
2841
|
+
const features = [];
|
|
2842
|
+
parentSource.getFeatures().forEach((feature, i) => {
|
|
2843
|
+
const geom = feature.getGeometry();
|
|
2844
|
+
if (!geom)
|
|
2845
|
+
return;
|
|
2846
|
+
if (!(geom instanceof LineString) && !(geom instanceof MultiLineString))
|
|
2847
|
+
return;
|
|
2848
|
+
const first = geom.getFirstCoordinate();
|
|
2849
|
+
const last = geom.getLastCoordinate();
|
|
2850
|
+
if (!first || !last || first.length < 2 || last.length < 2)
|
|
2851
|
+
return;
|
|
2852
|
+
const isCollapsed = first[0] === last[0] && first[1] === last[1];
|
|
2853
|
+
if (isCollapsed) {
|
|
2854
|
+
const style = collapsed ?? start ?? finish;
|
|
2855
|
+
if (style)
|
|
2856
|
+
features.push(makePoint(first, style, `__startfinish_collapsed_${i}`));
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
if (start)
|
|
2860
|
+
features.push(makePoint(first, start, `__startfinish_start_${i}`));
|
|
2861
|
+
if (finish)
|
|
2862
|
+
features.push(makePoint(last, finish, `__startfinish_finish_${i}`));
|
|
2863
|
+
});
|
|
2864
|
+
this.source.clear();
|
|
2865
|
+
if (features.length > 0) {
|
|
2866
|
+
this.source.addFeatures(features);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
syncVisibility() {
|
|
2870
|
+
this.layer.setVisible(this.parentLayer.getVisible());
|
|
2871
|
+
}
|
|
2872
|
+
syncOpacity() {
|
|
2873
|
+
this.layer.setOpacity(this.parentLayer.getOpacity());
|
|
2874
|
+
}
|
|
2875
|
+
dispose() {
|
|
2876
|
+
if (this.rafId !== null) {
|
|
2877
|
+
cancelAnimationFrame(this.rafId);
|
|
2878
|
+
this.rafId = null;
|
|
2879
|
+
}
|
|
2880
|
+
unByKey(this.moveEndKey);
|
|
2881
|
+
unByKey(this.visibilityKey);
|
|
2882
|
+
this.unsubCollection();
|
|
2883
|
+
this.unsubChanges?.();
|
|
2884
|
+
this.map.removeLayer(this.layer);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2668
2888
|
class LayerManager {
|
|
2669
2889
|
map;
|
|
2670
2890
|
layers = {};
|
|
@@ -2740,6 +2960,17 @@ class LayerManager {
|
|
|
2740
2960
|
});
|
|
2741
2961
|
this.decorationManagers.push(decorationManager);
|
|
2742
2962
|
}
|
|
2963
|
+
if (descriptor.feature.decorations?.startFinish) {
|
|
2964
|
+
const sf = descriptor.feature.decorations.startFinish;
|
|
2965
|
+
if (sf.start || sf.finish || sf.collapsed) {
|
|
2966
|
+
this.decorationManagers.push(new StartFinishDecorationManager({
|
|
2967
|
+
map: this.map,
|
|
2968
|
+
parentLayer: layer,
|
|
2969
|
+
parentApi: api,
|
|
2970
|
+
config: sf,
|
|
2971
|
+
}));
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2743
2974
|
});
|
|
2744
2975
|
this.interactions = new InteractionManager({
|
|
2745
2976
|
ctx,
|