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.
@@ -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,