@angular-helpers/openlayers 0.2.0 → 0.4.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/README.md +169 -2
- package/fesm2022/angular-helpers-openlayers-core.mjs +18 -0
- package/fesm2022/angular-helpers-openlayers-interactions.mjs +12 -0
- package/fesm2022/angular-helpers-openlayers-layers.mjs +141 -13
- package/fesm2022/angular-helpers-openlayers-military.mjs +223 -6
- package/fesm2022/angular-helpers-openlayers-overlays.mjs +439 -8
- package/package.json +6 -2
- package/types/angular-helpers-openlayers-core.d.ts +17 -0
- package/types/angular-helpers-openlayers-interactions.d.ts +7 -0
- package/types/angular-helpers-openlayers-layers.d.ts +48 -34
- package/types/angular-helpers-openlayers-military.d.ts +156 -2
- package/types/angular-helpers-openlayers-overlays.d.ts +196 -11
|
@@ -1,22 +1,453 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable } from '@angular/core';
|
|
2
|
+
import { inject, ElementRef, input, output, DestroyRef, effect, ChangeDetectionStrategy, Component, Directive, EnvironmentInjector, ApplicationRef, createComponent, Injectable } from '@angular/core';
|
|
3
|
+
import Overlay from 'ol/Overlay';
|
|
4
|
+
import { OlMapService, OlZoneHelper } from '@angular-helpers/openlayers/core';
|
|
5
|
+
import { OlLayerService } from '@angular-helpers/openlayers/layers';
|
|
6
|
+
|
|
7
|
+
// OlPopupComponent — declarative popup with content projection.
|
|
8
|
+
/**
|
|
9
|
+
* Declarative map popup that projects arbitrary Angular content via `<ng-content>`.
|
|
10
|
+
*
|
|
11
|
+
* The component's host element is used directly as the `ol/Overlay`'s element, so
|
|
12
|
+
* projected children stay inside Angular's component tree and benefit from change
|
|
13
|
+
* detection without any extra plumbing.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```html
|
|
17
|
+
* <ol-popup
|
|
18
|
+
* [position]="selectedCoord()"
|
|
19
|
+
* [closeButton]="true"
|
|
20
|
+
* [autoPan]="true"
|
|
21
|
+
* (closed)="clearSelection()"
|
|
22
|
+
* >
|
|
23
|
+
* <h3>{{ selected()?.name }}</h3>
|
|
24
|
+
* <p>{{ selected()?.description }}</p>
|
|
25
|
+
* </ol-popup>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
class OlPopupComponent {
|
|
29
|
+
elementRef = inject(ElementRef);
|
|
30
|
+
mapService = inject(OlMapService);
|
|
31
|
+
zoneHelper = inject(OlZoneHelper);
|
|
32
|
+
/** Map coordinate where the popup is anchored. `null` hides the popup. */
|
|
33
|
+
position = input(null, ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
34
|
+
/** Pixel offset relative to `position`. */
|
|
35
|
+
offset = input([0, 0], ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
|
|
36
|
+
/** Anchor of the popup element relative to `position`. */
|
|
37
|
+
positioning = input('bottom-center', ...(ngDevMode ? [{ debugName: "positioning" }] : /* istanbul ignore next */ []));
|
|
38
|
+
/** Whether the map auto-pans to keep the popup in view. */
|
|
39
|
+
autoPan = input(false, ...(ngDevMode ? [{ debugName: "autoPan" }] : /* istanbul ignore next */ []));
|
|
40
|
+
/** Whether to render the default close button. */
|
|
41
|
+
closeButton = input(false, ...(ngDevMode ? [{ debugName: "closeButton" }] : /* istanbul ignore next */ []));
|
|
42
|
+
/** Emitted when the popup transitions from visible to hidden (close button or `position` set to null). */
|
|
43
|
+
closed = output();
|
|
44
|
+
overlay = null;
|
|
45
|
+
currentMap = null;
|
|
46
|
+
wasVisible = false;
|
|
47
|
+
constructor() {
|
|
48
|
+
inject(DestroyRef).onDestroy(() => this.dispose());
|
|
49
|
+
// Create the overlay lazily when the map becomes ready, then keep it in sync
|
|
50
|
+
// with the inputs. The overlay's element is THIS component's host element so
|
|
51
|
+
// projected content lives inside Angular's view tree.
|
|
52
|
+
this.mapService.onReady((map) => {
|
|
53
|
+
this.currentMap = map;
|
|
54
|
+
this.zoneHelper.runOutsideAngular(() => {
|
|
55
|
+
this.overlay = new Overlay({
|
|
56
|
+
element: this.elementRef.nativeElement,
|
|
57
|
+
stopEvent: true,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
this.applyState();
|
|
61
|
+
});
|
|
62
|
+
effect(() => {
|
|
63
|
+
// Read every signal so the effect re-runs on any change.
|
|
64
|
+
this.position();
|
|
65
|
+
this.offset();
|
|
66
|
+
this.positioning();
|
|
67
|
+
this.autoPan();
|
|
68
|
+
this.applyState();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** @internal */
|
|
72
|
+
onCloseClick() {
|
|
73
|
+
this.zoneHelper.runInsideAngular(() => this.closed.emit());
|
|
74
|
+
}
|
|
75
|
+
applyState() {
|
|
76
|
+
const overlay = this.overlay;
|
|
77
|
+
const map = this.currentMap;
|
|
78
|
+
if (!overlay || !map)
|
|
79
|
+
return;
|
|
80
|
+
const position = this.position();
|
|
81
|
+
this.zoneHelper.runOutsideAngular(() => {
|
|
82
|
+
overlay.setOffset(this.offset());
|
|
83
|
+
overlay.setPositioning(this.positioning());
|
|
84
|
+
const wantsAutoPan = this.autoPan();
|
|
85
|
+
overlay.set('autoPan', wantsAutoPan);
|
|
86
|
+
const visible = position !== null;
|
|
87
|
+
if (visible && !this.wasVisible) {
|
|
88
|
+
map.addOverlay(overlay);
|
|
89
|
+
}
|
|
90
|
+
overlay.setPosition(visible ? position : undefined);
|
|
91
|
+
if (!visible && this.wasVisible) {
|
|
92
|
+
map.removeOverlay(overlay);
|
|
93
|
+
this.zoneHelper.runInsideAngular(() => this.closed.emit());
|
|
94
|
+
}
|
|
95
|
+
this.wasVisible = visible;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
dispose() {
|
|
99
|
+
if (!this.overlay || !this.currentMap)
|
|
100
|
+
return;
|
|
101
|
+
const overlay = this.overlay;
|
|
102
|
+
const map = this.currentMap;
|
|
103
|
+
this.zoneHelper.runOutsideAngular(() => {
|
|
104
|
+
if (this.wasVisible)
|
|
105
|
+
map.removeOverlay(overlay);
|
|
106
|
+
});
|
|
107
|
+
this.overlay = null;
|
|
108
|
+
this.currentMap = null;
|
|
109
|
+
}
|
|
110
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlPopupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
111
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: OlPopupComponent, isStandalone: true, selector: "ol-popup", inputs: { position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, positioning: { classPropertyName: "positioning", publicName: "positioning", isSignal: true, isRequired: false, transformFunction: null }, autoPan: { classPropertyName: "autoPan", publicName: "autoPan", isSignal: true, isRequired: false, transformFunction: null }, closeButton: { classPropertyName: "closeButton", publicName: "closeButton", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, host: { attributes: { "role": "dialog" } }, ngImport: i0, template: `
|
|
112
|
+
@if (closeButton()) {
|
|
113
|
+
<button type="button" class="ol-popup-close" aria-label="Close" (click)="onCloseClick()">
|
|
114
|
+
×
|
|
115
|
+
</button>
|
|
116
|
+
}
|
|
117
|
+
<ng-content />
|
|
118
|
+
`, isInline: true, styles: [":host{display:block;position:relative}.ol-popup-close{position:absolute;top:4px;right:4px;width:20px;height:20px;padding:0;border:none;background:transparent;font-size:16px;line-height:1;cursor:pointer;color:inherit}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
119
|
+
}
|
|
120
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlPopupComponent, decorators: [{
|
|
121
|
+
type: Component,
|
|
122
|
+
args: [{ selector: 'ol-popup', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
123
|
+
@if (closeButton()) {
|
|
124
|
+
<button type="button" class="ol-popup-close" aria-label="Close" (click)="onCloseClick()">
|
|
125
|
+
×
|
|
126
|
+
</button>
|
|
127
|
+
}
|
|
128
|
+
<ng-content />
|
|
129
|
+
`, host: {
|
|
130
|
+
role: 'dialog',
|
|
131
|
+
}, styles: [":host{display:block;position:relative}.ol-popup-close{position:absolute;top:4px;right:4px;width:20px;height:20px;padding:0;border:none;background:transparent;font-size:16px;line-height:1;cursor:pointer;color:inherit}\n"] }]
|
|
132
|
+
}], ctorParameters: () => [], propDecorators: { position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], positioning: [{ type: i0.Input, args: [{ isSignal: true, alias: "positioning", required: false }] }], autoPan: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoPan", required: false }] }], closeButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeButton", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
133
|
+
|
|
134
|
+
// OlTooltipDirective — feature hover tooltip on a vector layer.
|
|
135
|
+
/**
|
|
136
|
+
* Shows a floating tooltip whose text is read from a feature property when the
|
|
137
|
+
* pointer hovers over it. The tooltip element is appended to the map viewport
|
|
138
|
+
* and styled minimally; consumers can override via the `.ol-tooltip` CSS hook.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```html
|
|
142
|
+
* <ol-vector-layer
|
|
143
|
+
* id="cities"
|
|
144
|
+
* [features]="cities()"
|
|
145
|
+
* [olTooltip]="'name'"
|
|
146
|
+
* />
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* Apply `[olTooltipLayer]` to limit detection to a single layer (matches the
|
|
150
|
+
* value of the layer's `id` property). Without it the tooltip reacts to any
|
|
151
|
+
* feature found at the cursor.
|
|
152
|
+
*/
|
|
153
|
+
class OlTooltipDirective {
|
|
154
|
+
mapService = inject(OlMapService);
|
|
155
|
+
zoneHelper = inject(OlZoneHelper);
|
|
156
|
+
/** Property key to read from the hovered feature. */
|
|
157
|
+
olTooltip = input.required(...(ngDevMode ? [{ debugName: "olTooltip" }] : /* istanbul ignore next */ []));
|
|
158
|
+
/** Optional layer id; when set, only features on that layer trigger the tooltip. */
|
|
159
|
+
olTooltipLayer = input(null, ...(ngDevMode ? [{ debugName: "olTooltipLayer" }] : /* istanbul ignore next */ []));
|
|
160
|
+
element = null;
|
|
161
|
+
listener = null;
|
|
162
|
+
currentMap = null;
|
|
163
|
+
constructor() {
|
|
164
|
+
inject(DestroyRef).onDestroy(() => this.dispose());
|
|
165
|
+
this.mapService.onReady((map) => {
|
|
166
|
+
this.currentMap = map;
|
|
167
|
+
this.zoneHelper.runOutsideAngular(() => {
|
|
168
|
+
const viewport = map.getViewport();
|
|
169
|
+
const tooltip = document.createElement('div');
|
|
170
|
+
tooltip.setAttribute('role', 'tooltip');
|
|
171
|
+
tooltip.className = 'ol-tooltip';
|
|
172
|
+
Object.assign(tooltip.style, {
|
|
173
|
+
position: 'absolute',
|
|
174
|
+
pointerEvents: 'none',
|
|
175
|
+
padding: '4px 8px',
|
|
176
|
+
background: 'rgba(0, 0, 0, 0.75)',
|
|
177
|
+
color: '#fff',
|
|
178
|
+
fontSize: '12px',
|
|
179
|
+
borderRadius: '4px',
|
|
180
|
+
display: 'none',
|
|
181
|
+
whiteSpace: 'nowrap',
|
|
182
|
+
zIndex: '1000',
|
|
183
|
+
});
|
|
184
|
+
viewport.appendChild(tooltip);
|
|
185
|
+
this.element = tooltip;
|
|
186
|
+
const handler = (event) => {
|
|
187
|
+
this.handlePointerMove(event, map);
|
|
188
|
+
};
|
|
189
|
+
map.on('pointermove', handler);
|
|
190
|
+
this.listener = handler;
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
handlePointerMove(event, map) {
|
|
195
|
+
const tooltip = this.element;
|
|
196
|
+
if (!tooltip)
|
|
197
|
+
return;
|
|
198
|
+
const layerId = this.olTooltipLayer();
|
|
199
|
+
const propKey = this.olTooltip();
|
|
200
|
+
let matched = null;
|
|
201
|
+
map.forEachFeatureAtPixel(event.pixel, (feature) => {
|
|
202
|
+
matched = feature;
|
|
203
|
+
return true;
|
|
204
|
+
}, {
|
|
205
|
+
layerFilter: layerId ? (layer) => layer.get('id') === layerId : undefined,
|
|
206
|
+
});
|
|
207
|
+
if (!matched) {
|
|
208
|
+
tooltip.style.display = 'none';
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const value = matched.get(propKey);
|
|
212
|
+
if (value === undefined || value === null) {
|
|
213
|
+
tooltip.style.display = 'none';
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
tooltip.textContent = String(value);
|
|
217
|
+
tooltip.style.display = 'block';
|
|
218
|
+
const [x, y] = event.pixel;
|
|
219
|
+
tooltip.style.left = `${x + 12}px`;
|
|
220
|
+
tooltip.style.top = `${y + 12}px`;
|
|
221
|
+
}
|
|
222
|
+
dispose() {
|
|
223
|
+
const map = this.currentMap;
|
|
224
|
+
const listener = this.listener;
|
|
225
|
+
if (map && listener) {
|
|
226
|
+
this.zoneHelper.runOutsideAngular(() => map.un('pointermove', listener));
|
|
227
|
+
}
|
|
228
|
+
if (this.element?.parentNode) {
|
|
229
|
+
this.element.parentNode.removeChild(this.element);
|
|
230
|
+
}
|
|
231
|
+
this.element = null;
|
|
232
|
+
this.listener = null;
|
|
233
|
+
this.currentMap = null;
|
|
234
|
+
}
|
|
235
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
236
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.4", type: OlTooltipDirective, isStandalone: true, selector: "[olTooltip]", inputs: { olTooltip: { classPropertyName: "olTooltip", publicName: "olTooltip", isSignal: true, isRequired: true, transformFunction: null }, olTooltipLayer: { classPropertyName: "olTooltipLayer", publicName: "olTooltipLayer", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
|
|
237
|
+
}
|
|
238
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlTooltipDirective, decorators: [{
|
|
239
|
+
type: Directive,
|
|
240
|
+
args: [{
|
|
241
|
+
selector: '[olTooltip]',
|
|
242
|
+
}]
|
|
243
|
+
}], ctorParameters: () => [], propDecorators: { olTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "olTooltip", required: true }] }], olTooltipLayer: [{ type: i0.Input, args: [{ isSignal: true, alias: "olTooltipLayer", required: false }] }] } });
|
|
3
244
|
|
|
4
245
|
// OlPopupService
|
|
246
|
+
/**
|
|
247
|
+
* Manages OpenLayers popup overlays anchored to map coordinates.
|
|
248
|
+
*
|
|
249
|
+
* Three content modes:
|
|
250
|
+
* - `open()` — string text or `HTMLElement`
|
|
251
|
+
* - `openComponent()` — dynamic Angular component via `createComponent + hostElement`
|
|
252
|
+
* - The `<ol-popup>` component delegates `open()` internally for declarative usage.
|
|
253
|
+
*
|
|
254
|
+
* All calls are idempotent by `id`. Calls made before the underlying `ol/Map` is ready
|
|
255
|
+
* are queued and replayed in order once `OlMapService.onReady` fires.
|
|
256
|
+
*/
|
|
5
257
|
class OlPopupService {
|
|
6
|
-
|
|
7
|
-
|
|
258
|
+
mapService = inject(OlMapService);
|
|
259
|
+
zoneHelper = inject(OlZoneHelper);
|
|
260
|
+
envInjector = inject(EnvironmentInjector);
|
|
261
|
+
appRef = inject(ApplicationRef);
|
|
262
|
+
popups = new Map();
|
|
263
|
+
pending = [];
|
|
264
|
+
flushSubscribed = false;
|
|
265
|
+
constructor() {
|
|
266
|
+
inject(DestroyRef).onDestroy(() => this.closeAll());
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Open a popup with `string` or `HTMLElement` content.
|
|
270
|
+
*
|
|
271
|
+
* Idempotent by `id`: a second call with the same id updates `position` and
|
|
272
|
+
* `content` of the existing popup instead of creating a duplicate.
|
|
273
|
+
*/
|
|
274
|
+
open(options) {
|
|
275
|
+
const id = options.id ?? generateId('popup');
|
|
276
|
+
const map = this.mapService.getMap();
|
|
277
|
+
if (!map) {
|
|
278
|
+
this.queue({ kind: 'open', options: { ...options, id } });
|
|
279
|
+
return { id, close: () => this.close(id) };
|
|
280
|
+
}
|
|
281
|
+
this.createOrUpdate(id, options, map);
|
|
282
|
+
return { id, close: () => this.close(id) };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Open a popup whose content is a dynamically-instantiated Angular component.
|
|
286
|
+
*
|
|
287
|
+
* Uses `createComponent({ environmentInjector, hostElement, bindings, directives })`
|
|
288
|
+
* (Angular 16.2+) and attaches the host view to `ApplicationRef` so change detection
|
|
289
|
+
* reaches the rendered component.
|
|
290
|
+
*
|
|
291
|
+
* Idempotent by `id`: a previous `ComponentRef` registered under the same id is
|
|
292
|
+
* destroyed before the new one is created.
|
|
293
|
+
*/
|
|
294
|
+
openComponent(options) {
|
|
295
|
+
const id = options.id ?? generateId('popup');
|
|
296
|
+
const map = this.mapService.getMap();
|
|
297
|
+
if (!map) {
|
|
298
|
+
this.queue({
|
|
299
|
+
kind: 'openComponent',
|
|
300
|
+
options: { ...options, id },
|
|
301
|
+
});
|
|
302
|
+
return { id, close: () => this.close(id) };
|
|
303
|
+
}
|
|
304
|
+
this.createOrUpdateComponent(id, options, map);
|
|
305
|
+
return { id, close: () => this.close(id) };
|
|
306
|
+
}
|
|
307
|
+
/** Close and dispose a single popup by id. No-op when the id is unknown. */
|
|
308
|
+
close(id) {
|
|
309
|
+
const managed = this.popups.get(id);
|
|
310
|
+
if (!managed)
|
|
311
|
+
return;
|
|
312
|
+
managed.dispose();
|
|
313
|
+
this.popups.delete(id);
|
|
314
|
+
}
|
|
315
|
+
/** Close every managed popup. */
|
|
316
|
+
closeAll() {
|
|
317
|
+
const ids = [...this.popups.keys()];
|
|
318
|
+
for (const id of ids) {
|
|
319
|
+
this.close(id);
|
|
320
|
+
}
|
|
321
|
+
this.pending.length = 0;
|
|
322
|
+
}
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Internals
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
queue(call) {
|
|
327
|
+
this.pending.push(call);
|
|
328
|
+
if (this.flushSubscribed)
|
|
329
|
+
return;
|
|
330
|
+
this.flushSubscribed = true;
|
|
331
|
+
this.mapService.onReady((map) => this.flushPending(map));
|
|
332
|
+
}
|
|
333
|
+
flushPending(map) {
|
|
334
|
+
const pending = this.pending.splice(0);
|
|
335
|
+
for (const call of pending) {
|
|
336
|
+
if (call.kind === 'open') {
|
|
337
|
+
this.createOrUpdate(call.options.id, call.options, map);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
this.createOrUpdateComponent(call.options.id, call.options, map);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
createOrUpdate(id, options, map) {
|
|
345
|
+
const existing = this.popups.get(id);
|
|
346
|
+
if (existing && !existing.componentRef) {
|
|
347
|
+
// Update position / element of an existing string/HTMLElement popup in place.
|
|
348
|
+
this.zoneHelper.runOutsideAngular(() => {
|
|
349
|
+
existing.overlay.setPosition(options.position);
|
|
350
|
+
replaceElementContent(existing.overlay.getElement(), options.content);
|
|
351
|
+
applyOverlayConfig(existing.overlay, options);
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (existing) {
|
|
356
|
+
// Type changed (was a component popup) — dispose and recreate.
|
|
357
|
+
this.close(id);
|
|
358
|
+
}
|
|
359
|
+
this.zoneHelper.runOutsideAngular(() => {
|
|
360
|
+
const element = buildContentElement(options.content, options.className);
|
|
361
|
+
const overlay = new Overlay({
|
|
362
|
+
element,
|
|
363
|
+
position: options.position,
|
|
364
|
+
positioning: options.positioning ?? 'bottom-center',
|
|
365
|
+
offset: options.offset ?? [0, 0],
|
|
366
|
+
autoPan: options.autoPan ?? false,
|
|
367
|
+
});
|
|
368
|
+
map.addOverlay(overlay);
|
|
369
|
+
const dispose = () => {
|
|
370
|
+
this.zoneHelper.runOutsideAngular(() => {
|
|
371
|
+
map.removeOverlay(overlay);
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
this.popups.set(id, { id, overlay, dispose });
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
createOrUpdateComponent(id, options, map) {
|
|
378
|
+
// Always recreate component popups on a second call — simpler and safer than
|
|
379
|
+
// trying to re-bind inputs on an already-created ref.
|
|
380
|
+
if (this.popups.has(id)) {
|
|
381
|
+
this.close(id);
|
|
382
|
+
}
|
|
383
|
+
this.zoneHelper.runOutsideAngular(() => {
|
|
384
|
+
const hostElement = document.createElement('div');
|
|
385
|
+
if (options.className)
|
|
386
|
+
hostElement.className = options.className;
|
|
387
|
+
const ref = createComponent(options.component, {
|
|
388
|
+
environmentInjector: this.envInjector,
|
|
389
|
+
elementInjector: options.injector,
|
|
390
|
+
hostElement,
|
|
391
|
+
bindings: options.bindings,
|
|
392
|
+
directives: options.directives?.map((d) => typeof d === 'function' ? d : { type: d.type, bindings: d.bindings ?? [] }),
|
|
393
|
+
});
|
|
394
|
+
this.appRef.attachView(ref.hostView);
|
|
395
|
+
const overlay = new Overlay({
|
|
396
|
+
element: hostElement,
|
|
397
|
+
position: options.position,
|
|
398
|
+
positioning: options.positioning ?? 'bottom-center',
|
|
399
|
+
offset: options.offset ?? [0, 0],
|
|
400
|
+
autoPan: options.autoPan ?? false,
|
|
401
|
+
});
|
|
402
|
+
map.addOverlay(overlay);
|
|
403
|
+
const dispose = () => {
|
|
404
|
+
this.zoneHelper.runOutsideAngular(() => {
|
|
405
|
+
map.removeOverlay(overlay);
|
|
406
|
+
this.appRef.detachView(ref.hostView);
|
|
407
|
+
ref.destroy();
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
this.popups.set(id, { id, overlay, componentRef: ref, dispose });
|
|
411
|
+
});
|
|
8
412
|
}
|
|
9
|
-
hidePopup(id) { }
|
|
10
|
-
hideAllPopups() { }
|
|
11
413
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlPopupService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
12
414
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlPopupService });
|
|
13
415
|
}
|
|
14
416
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlPopupService, decorators: [{
|
|
15
417
|
type: Injectable
|
|
16
|
-
}] });
|
|
418
|
+
}], ctorParameters: () => [] });
|
|
419
|
+
// -----------------------------------------------------------------------------
|
|
420
|
+
// Helpers
|
|
421
|
+
// -----------------------------------------------------------------------------
|
|
422
|
+
function generateId(prefix) {
|
|
423
|
+
return `${prefix}-${Math.random().toString(36).slice(2, 10)}`;
|
|
424
|
+
}
|
|
425
|
+
function buildContentElement(content, className) {
|
|
426
|
+
const wrapper = document.createElement('div');
|
|
427
|
+
if (className)
|
|
428
|
+
wrapper.className = className;
|
|
429
|
+
replaceElementContent(wrapper, content);
|
|
430
|
+
return wrapper;
|
|
431
|
+
}
|
|
432
|
+
function replaceElementContent(element, content) {
|
|
433
|
+
while (element.firstChild)
|
|
434
|
+
element.removeChild(element.firstChild);
|
|
435
|
+
if (typeof content === 'string') {
|
|
436
|
+
element.textContent = content;
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
element.appendChild(content);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function applyOverlayConfig(overlay, config) {
|
|
443
|
+
if (config.positioning)
|
|
444
|
+
overlay.setPositioning(config.positioning);
|
|
445
|
+
if (config.offset)
|
|
446
|
+
overlay.setOffset(config.offset);
|
|
447
|
+
}
|
|
17
448
|
|
|
18
449
|
function withOverlays() {
|
|
19
|
-
return { kind: 'overlays', providers: [OlPopupService] };
|
|
450
|
+
return { kind: 'overlays', providers: [OlLayerService, OlPopupService] };
|
|
20
451
|
}
|
|
21
452
|
function provideOverlays() {
|
|
22
453
|
return withOverlays();
|
|
@@ -28,4 +459,4 @@ function provideOverlays() {
|
|
|
28
459
|
* Generated bundle index. Do not edit.
|
|
29
460
|
*/
|
|
30
461
|
|
|
31
|
-
export { OlPopupService, provideOverlays, withOverlays };
|
|
462
|
+
export { OlPopupComponent, OlPopupService, OlTooltipDirective, provideOverlays, withOverlays };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-helpers/openlayers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Modern Angular wrapper for OpenLayers with modular architecture, standalone components, and hybrid template/programmatic API",
|
|
5
5
|
"homepage": "https://gaspar1992.github.io/angular-helpers/docs/openlayers",
|
|
6
6
|
"repository": {
|
|
@@ -30,11 +30,15 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@angular/core": "^21.0.0",
|
|
34
33
|
"@angular/common": "^21.0.0",
|
|
34
|
+
"@angular/core": "^21.0.0",
|
|
35
|
+
"milsymbol": "^3.0.0",
|
|
35
36
|
"ol": "^10.0.0"
|
|
36
37
|
},
|
|
37
38
|
"peerDependenciesMeta": {
|
|
39
|
+
"milsymbol": {
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
38
42
|
"ol": {
|
|
39
43
|
"optional": false
|
|
40
44
|
}
|
|
@@ -36,6 +36,22 @@ interface Style {
|
|
|
36
36
|
width?: number;
|
|
37
37
|
};
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Icon style for Point features. When set, renderers should produce an
|
|
41
|
+
* `ol/style/Icon` with the given source. Used by the military symbology
|
|
42
|
+
* helpers to embed `milsymbol`-generated SVGs as data URLs.
|
|
43
|
+
*/
|
|
44
|
+
icon?: {
|
|
45
|
+
/** Image source — typically a `data:image/svg+xml;base64,...` URL */
|
|
46
|
+
src: string;
|
|
47
|
+
/** Pixel size `[width, height]` */
|
|
48
|
+
size?: [number, number];
|
|
49
|
+
/**
|
|
50
|
+
* Anchor in fractional coordinates `[x, y]` where `0..1` is the icon
|
|
51
|
+
* extent. `[0.5, 0.5]` centers the icon on the feature.
|
|
52
|
+
*/
|
|
53
|
+
anchor?: [number, number];
|
|
54
|
+
};
|
|
39
55
|
text?: {
|
|
40
56
|
text?: string;
|
|
41
57
|
font?: string;
|
|
@@ -83,6 +99,7 @@ declare class OlMapComponent {
|
|
|
83
99
|
mapDblClick: _angular_core.OutputEmitterRef<MapClickEvent>;
|
|
84
100
|
mapContainerRef: _angular_core.Signal<ElementRef<HTMLDivElement>>;
|
|
85
101
|
private map?;
|
|
102
|
+
private resizeObserver?;
|
|
86
103
|
constructor();
|
|
87
104
|
private initMap;
|
|
88
105
|
private destroyMap;
|
|
@@ -11,6 +11,12 @@ import { Feature as Feature$1 } from 'ol';
|
|
|
11
11
|
type InteractionType = 'select' | 'draw' | 'modify' | 'dragAndDrop';
|
|
12
12
|
interface InteractionConfig {
|
|
13
13
|
active?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* If true, enabling this interaction will automatically disable other active exclusive interactions.
|
|
16
|
+
* Useful for mutually exclusive tools like Draw and Modify.
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
exclusive?: boolean;
|
|
14
20
|
}
|
|
15
21
|
interface SelectConfig extends InteractionConfig {
|
|
16
22
|
layers?: string[];
|
|
@@ -178,6 +184,7 @@ declare class InteractionStateService {
|
|
|
178
184
|
readonly modify$: rxjs.Observable<ModifyEvent>;
|
|
179
185
|
/**
|
|
180
186
|
* Adds a managed interaction to the state.
|
|
187
|
+
* If the interaction is marked as exclusive, it disables other exclusive interactions.
|
|
181
188
|
* @param interaction - The interaction to add
|
|
182
189
|
*/
|
|
183
190
|
addInteraction(interaction: ManagedInteraction): void;
|
|
@@ -1,7 +1,52 @@
|
|
|
1
1
|
import * as _angular_core from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { Layer, Extent, Feature, Style, OlFeature } from '@angular-helpers/openlayers/core';
|
|
3
3
|
import BaseLayer from 'ol/layer/Base';
|
|
4
4
|
|
|
5
|
+
interface LayerConfig extends Layer {
|
|
6
|
+
type: 'vector' | 'tile' | 'image';
|
|
7
|
+
extent?: Extent;
|
|
8
|
+
minResolution?: number;
|
|
9
|
+
maxResolution?: number;
|
|
10
|
+
}
|
|
11
|
+
interface ClusterConfig {
|
|
12
|
+
/** Enable clustering (default: false) */
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
/** Distance in pixels within which features will be clustered (default: 40) */
|
|
15
|
+
distance?: number;
|
|
16
|
+
/** Minimum distance between clusters (default: 20) */
|
|
17
|
+
minDistance?: number;
|
|
18
|
+
/** Show count badge on cluster (default: true) */
|
|
19
|
+
showCount?: boolean;
|
|
20
|
+
/** Style for individual features when clustering */
|
|
21
|
+
featureStyle?: Style;
|
|
22
|
+
}
|
|
23
|
+
interface VectorLayerConfig extends LayerConfig {
|
|
24
|
+
type: 'vector';
|
|
25
|
+
features?: Feature[];
|
|
26
|
+
style?: Style | ((feature: Feature) => Style);
|
|
27
|
+
cluster?: ClusterConfig;
|
|
28
|
+
}
|
|
29
|
+
interface TileLayerConfig extends LayerConfig {
|
|
30
|
+
type: 'tile';
|
|
31
|
+
source: SourceConfig;
|
|
32
|
+
}
|
|
33
|
+
interface ImageLayerConfig extends LayerConfig {
|
|
34
|
+
type: 'image';
|
|
35
|
+
source: ImageSourceConfig;
|
|
36
|
+
}
|
|
37
|
+
interface SourceConfig {
|
|
38
|
+
type: 'osm' | 'xyz' | 'wms' | 'wmts';
|
|
39
|
+
url?: string;
|
|
40
|
+
attributions?: string | string[];
|
|
41
|
+
params?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
interface ImageSourceConfig {
|
|
44
|
+
type: 'wms' | 'static';
|
|
45
|
+
url: string;
|
|
46
|
+
params?: Record<string, unknown>;
|
|
47
|
+
imageExtent?: Extent;
|
|
48
|
+
}
|
|
49
|
+
|
|
5
50
|
declare class OlVectorLayerComponent {
|
|
6
51
|
private layerService;
|
|
7
52
|
private destroyRef;
|
|
@@ -11,9 +56,10 @@ declare class OlVectorLayerComponent {
|
|
|
11
56
|
opacity: _angular_core.InputSignal<number>;
|
|
12
57
|
visible: _angular_core.InputSignal<boolean>;
|
|
13
58
|
style: _angular_core.InputSignal<Style | ((feature: Feature) => Style)>;
|
|
59
|
+
cluster: _angular_core.InputSignal<ClusterConfig>;
|
|
14
60
|
constructor();
|
|
15
61
|
static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlVectorLayerComponent, never>;
|
|
16
|
-
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlVectorLayerComponent, "ol-vector-layer", never, { "id": { "alias": "id"; "required": true; "isSignal": true; }; "features": { "alias": "features"; "required": false; "isSignal": true; }; "zIndex": { "alias": "zIndex"; "required": false; "isSignal": true; }; "opacity": { "alias": "opacity"; "required": false; "isSignal": true; }; "visible": { "alias": "visible"; "required": false; "isSignal": true; }; "style": { "alias": "style"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
62
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlVectorLayerComponent, "ol-vector-layer", never, { "id": { "alias": "id"; "required": true; "isSignal": true; }; "features": { "alias": "features"; "required": false; "isSignal": true; }; "zIndex": { "alias": "zIndex"; "required": false; "isSignal": true; }; "opacity": { "alias": "opacity"; "required": false; "isSignal": true; }; "visible": { "alias": "visible"; "required": false; "isSignal": true; }; "style": { "alias": "style"; "required": false; "isSignal": true; }; "cluster": { "alias": "cluster"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
17
63
|
}
|
|
18
64
|
|
|
19
65
|
declare class OlTileLayerComponent {
|
|
@@ -48,38 +94,6 @@ declare class OlImageLayerComponent {
|
|
|
48
94
|
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlImageLayerComponent, "ol-image-layer", never, { "id": { "alias": "id"; "required": true; "isSignal": true; }; "sourceType": { "alias": "sourceType"; "required": true; "isSignal": true; }; "url": { "alias": "url"; "required": true; "isSignal": true; }; "params": { "alias": "params"; "required": false; "isSignal": true; }; "imageExtent": { "alias": "imageExtent"; "required": false; "isSignal": true; }; "zIndex": { "alias": "zIndex"; "required": false; "isSignal": true; }; "opacity": { "alias": "opacity"; "required": false; "isSignal": true; }; "visible": { "alias": "visible"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
49
95
|
}
|
|
50
96
|
|
|
51
|
-
interface LayerConfig extends Layer {
|
|
52
|
-
type: 'vector' | 'tile' | 'image';
|
|
53
|
-
extent?: Extent;
|
|
54
|
-
minResolution?: number;
|
|
55
|
-
maxResolution?: number;
|
|
56
|
-
}
|
|
57
|
-
interface VectorLayerConfig extends LayerConfig {
|
|
58
|
-
type: 'vector';
|
|
59
|
-
features?: Feature[];
|
|
60
|
-
style?: Style | ((feature: Feature) => Style);
|
|
61
|
-
}
|
|
62
|
-
interface TileLayerConfig extends LayerConfig {
|
|
63
|
-
type: 'tile';
|
|
64
|
-
source: SourceConfig;
|
|
65
|
-
}
|
|
66
|
-
interface ImageLayerConfig extends LayerConfig {
|
|
67
|
-
type: 'image';
|
|
68
|
-
source: ImageSourceConfig;
|
|
69
|
-
}
|
|
70
|
-
interface SourceConfig {
|
|
71
|
-
type: 'osm' | 'xyz' | 'wms' | 'wmts';
|
|
72
|
-
url?: string;
|
|
73
|
-
attributions?: string | string[];
|
|
74
|
-
params?: Record<string, unknown>;
|
|
75
|
-
}
|
|
76
|
-
interface ImageSourceConfig {
|
|
77
|
-
type: 'wms' | 'static';
|
|
78
|
-
url: string;
|
|
79
|
-
params?: Record<string, unknown>;
|
|
80
|
-
imageExtent?: Extent;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
97
|
interface LayerInfo {
|
|
84
98
|
id: string;
|
|
85
99
|
type: 'vector' | 'tile' | 'image';
|