@angular-helpers/openlayers 0.1.1 → 0.3.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.
@@ -1,43 +1,41 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, NgZone, input, ChangeDetectionStrategy, Component, Injectable } from '@angular/core';
3
- import { OlMapService } from '@angular-helpers/openlayers/core';
2
+ import { inject, input, DestroyRef, afterNextRender, ChangeDetectionStrategy, Component, InjectionToken, output, signal, Injectable } from '@angular/core';
3
+ import { OlMapService, OlZoneHelper } from '@angular-helpers/openlayers/core';
4
4
  import Zoom from 'ol/control/Zoom';
5
5
  import Attribution from 'ol/control/Attribution';
6
6
  import ScaleLine from 'ol/control/ScaleLine';
7
7
  import FullScreen from 'ol/control/FullScreen';
8
+ import Rotate from 'ol/control/Rotate';
9
+ import { CommonModule } from '@angular/common';
8
10
 
9
11
  // OlZoomControlComponent
10
12
  class OlZoomControlComponent {
11
13
  mapService = inject(OlMapService);
12
- ngZone = inject(NgZone);
14
+ zoneHelper = inject(OlZoneHelper);
13
15
  delta = input(1, ...(ngDevMode ? [{ debugName: "delta" }] : /* istanbul ignore next */ []));
14
16
  duration = input(250, ...(ngDevMode ? [{ debugName: "duration" }] : /* istanbul ignore next */ []));
15
17
  control;
16
- ngOnInit() {
17
- this.tryAddControl();
18
- }
19
- tryAddControl(retryCount = 0) {
20
- const map = this.mapService.getMap();
21
- if (!map) {
22
- if (retryCount < 10) {
23
- setTimeout(() => this.tryAddControl(retryCount + 1), Math.min(50 * (retryCount + 1), 500));
18
+ constructor() {
19
+ const destroyRef = inject(DestroyRef);
20
+ let destroyed = false;
21
+ destroyRef.onDestroy(() => {
22
+ if (this.control) {
23
+ const map = this.mapService.getMap();
24
+ if (map)
25
+ this.zoneHelper.runOutsideAngular(() => map.removeControl(this.control));
24
26
  }
25
- return;
26
- }
27
- this.ngZone.runOutsideAngular(() => {
28
- this.control = new Zoom({
29
- delta: this.delta(),
30
- duration: this.duration(),
31
- });
32
- map.addControl(this.control);
27
+ destroyed = true;
33
28
  });
34
- }
35
- ngOnDestroy() {
36
- const map = this.mapService.getMap();
37
- if (!this.control || !map)
38
- return;
39
- this.ngZone.runOutsideAngular(() => {
40
- map.removeControl(this.control);
29
+ afterNextRender(() => {
30
+ if (destroyed)
31
+ return;
32
+ const map = this.mapService.getMap();
33
+ if (!map)
34
+ return;
35
+ this.zoneHelper.runOutsideAngular(() => {
36
+ this.control = new Zoom({ delta: this.delta(), duration: this.duration() });
37
+ map.addControl(this.control);
38
+ });
41
39
  });
42
40
  }
43
41
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlZoomControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -50,40 +48,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
50
48
  template: '',
51
49
  changeDetection: ChangeDetectionStrategy.OnPush,
52
50
  }]
53
- }], propDecorators: { delta: [{ type: i0.Input, args: [{ isSignal: true, alias: "delta", required: false }] }], duration: [{ type: i0.Input, args: [{ isSignal: true, alias: "duration", required: false }] }] } });
51
+ }], ctorParameters: () => [], propDecorators: { delta: [{ type: i0.Input, args: [{ isSignal: true, alias: "delta", required: false }] }], duration: [{ type: i0.Input, args: [{ isSignal: true, alias: "duration", required: false }] }] } });
54
52
 
55
53
  // OlAttributionControlComponent
56
54
  class OlAttributionControlComponent {
57
55
  mapService = inject(OlMapService);
58
- ngZone = inject(NgZone);
56
+ zoneHelper = inject(OlZoneHelper);
59
57
  collapsible = input(true, ...(ngDevMode ? [{ debugName: "collapsible" }] : /* istanbul ignore next */ []));
60
58
  collapsed = input(true, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
61
59
  control;
62
- ngOnInit() {
63
- this.tryAddControl();
64
- }
65
- tryAddControl(retryCount = 0) {
66
- const map = this.mapService.getMap();
67
- if (!map) {
68
- if (retryCount < 10) {
69
- setTimeout(() => this.tryAddControl(retryCount + 1), Math.min(50 * (retryCount + 1), 500));
60
+ constructor() {
61
+ const destroyRef = inject(DestroyRef);
62
+ let destroyed = false;
63
+ destroyRef.onDestroy(() => {
64
+ if (this.control) {
65
+ const map = this.mapService.getMap();
66
+ if (map)
67
+ this.zoneHelper.runOutsideAngular(() => map.removeControl(this.control));
70
68
  }
71
- return;
72
- }
73
- this.ngZone.runOutsideAngular(() => {
74
- this.control = new Attribution({
75
- collapsible: this.collapsible(),
76
- collapsed: this.collapsed(),
77
- });
78
- map.addControl(this.control);
69
+ destroyed = true;
79
70
  });
80
- }
81
- ngOnDestroy() {
82
- const map = this.mapService.getMap();
83
- if (!this.control || !map)
84
- return;
85
- this.ngZone.runOutsideAngular(() => {
86
- map.removeControl(this.control);
71
+ afterNextRender(() => {
72
+ if (destroyed)
73
+ return;
74
+ const map = this.mapService.getMap();
75
+ if (!map)
76
+ return;
77
+ this.zoneHelper.runOutsideAngular(() => {
78
+ this.control = new Attribution({
79
+ collapsible: this.collapsible(),
80
+ collapsed: this.collapsed(),
81
+ });
82
+ map.addControl(this.control);
83
+ });
87
84
  });
88
85
  }
89
86
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlAttributionControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -96,42 +93,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
96
93
  template: '',
97
94
  changeDetection: ChangeDetectionStrategy.OnPush,
98
95
  }]
99
- }], propDecorators: { collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }] } });
96
+ }], ctorParameters: () => [], propDecorators: { collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }] } });
100
97
 
101
98
  // OlScaleLineControlComponent
102
99
  class OlScaleLineControlComponent {
103
100
  mapService = inject(OlMapService);
104
- ngZone = inject(NgZone);
101
+ zoneHelper = inject(OlZoneHelper);
105
102
  units = input('metric', ...(ngDevMode ? [{ debugName: "units" }] : /* istanbul ignore next */ []));
106
103
  bar = input(false, ...(ngDevMode ? [{ debugName: "bar" }] : /* istanbul ignore next */ []));
107
104
  steps = input(4, ...(ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []));
108
105
  control;
109
- ngOnInit() {
110
- this.tryAddControl();
111
- }
112
- tryAddControl(retryCount = 0) {
113
- const map = this.mapService.getMap();
114
- if (!map) {
115
- if (retryCount < 10) {
116
- setTimeout(() => this.tryAddControl(retryCount + 1), Math.min(50 * (retryCount + 1), 500));
106
+ constructor() {
107
+ const destroyRef = inject(DestroyRef);
108
+ let destroyed = false;
109
+ destroyRef.onDestroy(() => {
110
+ if (this.control) {
111
+ const map = this.mapService.getMap();
112
+ if (map)
113
+ this.zoneHelper.runOutsideAngular(() => map.removeControl(this.control));
117
114
  }
118
- return;
119
- }
120
- this.ngZone.runOutsideAngular(() => {
121
- this.control = new ScaleLine({
122
- units: this.units(),
123
- bar: this.bar(),
124
- steps: this.steps(),
125
- });
126
- map.addControl(this.control);
115
+ destroyed = true;
127
116
  });
128
- }
129
- ngOnDestroy() {
130
- const map = this.mapService.getMap();
131
- if (!this.control || !map)
132
- return;
133
- this.ngZone.runOutsideAngular(() => {
134
- map.removeControl(this.control);
117
+ afterNextRender(() => {
118
+ if (destroyed)
119
+ return;
120
+ const map = this.mapService.getMap();
121
+ if (!map)
122
+ return;
123
+ this.zoneHelper.runOutsideAngular(() => {
124
+ this.control = new ScaleLine({
125
+ units: this.units(),
126
+ bar: this.bar(),
127
+ steps: this.steps(),
128
+ });
129
+ map.addControl(this.control);
130
+ });
135
131
  });
136
132
  }
137
133
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlScaleLineControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -144,44 +140,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
144
140
  template: '',
145
141
  changeDetection: ChangeDetectionStrategy.OnPush,
146
142
  }]
147
- }], propDecorators: { units: [{ type: i0.Input, args: [{ isSignal: true, alias: "units", required: false }] }], bar: [{ type: i0.Input, args: [{ isSignal: true, alias: "bar", required: false }] }], steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: false }] }] } });
143
+ }], ctorParameters: () => [], propDecorators: { units: [{ type: i0.Input, args: [{ isSignal: true, alias: "units", required: false }] }], bar: [{ type: i0.Input, args: [{ isSignal: true, alias: "bar", required: false }] }], steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: false }] }] } });
148
144
 
149
145
  // OlFullscreenControlComponent
150
146
  class OlFullscreenControlComponent {
151
147
  mapService = inject(OlMapService);
152
- ngZone = inject(NgZone);
148
+ zoneHelper = inject(OlZoneHelper);
153
149
  source = input(...(ngDevMode ? [undefined, { debugName: "source" }] : /* istanbul ignore next */ []));
154
150
  label = input('⤢', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
155
151
  labelActive = input('⤡', ...(ngDevMode ? [{ debugName: "labelActive" }] : /* istanbul ignore next */ []));
156
152
  tipLabel = input('Toggle full-screen', ...(ngDevMode ? [{ debugName: "tipLabel" }] : /* istanbul ignore next */ []));
157
153
  control;
158
- ngOnInit() {
159
- this.tryAddControl();
160
- }
161
- tryAddControl(retryCount = 0) {
162
- const map = this.mapService.getMap();
163
- if (!map) {
164
- if (retryCount < 10) {
165
- setTimeout(() => this.tryAddControl(retryCount + 1), Math.min(50 * (retryCount + 1), 500));
154
+ constructor() {
155
+ const destroyRef = inject(DestroyRef);
156
+ let destroyed = false;
157
+ destroyRef.onDestroy(() => {
158
+ if (this.control) {
159
+ const map = this.mapService.getMap();
160
+ if (map)
161
+ this.zoneHelper.runOutsideAngular(() => map.removeControl(this.control));
166
162
  }
167
- return;
168
- }
169
- this.ngZone.runOutsideAngular(() => {
170
- this.control = new FullScreen({
171
- source: this.source(),
172
- label: this.label(),
173
- labelActive: this.labelActive(),
174
- tipLabel: this.tipLabel(),
175
- });
176
- map.addControl(this.control);
163
+ destroyed = true;
177
164
  });
178
- }
179
- ngOnDestroy() {
180
- const map = this.mapService.getMap();
181
- if (!this.control || !map)
182
- return;
183
- this.ngZone.runOutsideAngular(() => {
184
- map.removeControl(this.control);
165
+ afterNextRender(() => {
166
+ if (destroyed)
167
+ return;
168
+ const map = this.mapService.getMap();
169
+ if (!map)
170
+ return;
171
+ this.zoneHelper.runOutsideAngular(() => {
172
+ this.control = new FullScreen({
173
+ source: this.source(),
174
+ label: this.label(),
175
+ labelActive: this.labelActive(),
176
+ tipLabel: this.tipLabel(),
177
+ });
178
+ map.addControl(this.control);
179
+ });
185
180
  });
186
181
  }
187
182
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlFullscreenControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -194,7 +189,384 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
194
189
  template: '',
195
190
  changeDetection: ChangeDetectionStrategy.OnPush,
196
191
  }]
197
- }], propDecorators: { source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], labelActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelActive", required: false }] }], tipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tipLabel", required: false }] }] } });
192
+ }], ctorParameters: () => [], propDecorators: { source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], labelActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelActive", required: false }] }], tipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tipLabel", required: false }] }] } });
193
+
194
+ // OlRotateControlComponent
195
+ /**
196
+ * Injection token for map service used by rotate control
197
+ * Consumers should provide their OlMapService using this token:
198
+ * ```ts
199
+ * { provide: ROTATE_CONTROL_MAP_SERVICE, useExisting: OlMapService }
200
+ * ```
201
+ */
202
+ const ROTATE_CONTROL_MAP_SERVICE = new InjectionToken('ROTATE_CONTROL_MAP_SERVICE');
203
+ class OlRotateControlComponent {
204
+ mapService = inject(ROTATE_CONTROL_MAP_SERVICE, { optional: true });
205
+ zoneHelper = inject(OlZoneHelper);
206
+ autoHide = input(true, ...(ngDevMode ? [{ debugName: "autoHide" }] : /* istanbul ignore next */ []));
207
+ duration = input(250, ...(ngDevMode ? [{ debugName: "duration" }] : /* istanbul ignore next */ []));
208
+ tipLabel = input('Reset rotation', ...(ngDevMode ? [{ debugName: "tipLabel" }] : /* istanbul ignore next */ []));
209
+ control;
210
+ constructor() {
211
+ if (!this.mapService)
212
+ return;
213
+ const destroyRef = inject(DestroyRef);
214
+ let destroyed = false;
215
+ destroyRef.onDestroy(() => {
216
+ if (this.control) {
217
+ const map = this.mapService.getMap();
218
+ if (map)
219
+ this.zoneHelper.runOutsideAngular(() => map.removeControl(this.control));
220
+ }
221
+ destroyed = true;
222
+ });
223
+ afterNextRender(() => {
224
+ if (destroyed)
225
+ return;
226
+ const map = this.mapService.getMap();
227
+ if (!map)
228
+ return;
229
+ this.zoneHelper.runOutsideAngular(() => {
230
+ this.control = new Rotate({
231
+ autoHide: this.autoHide(),
232
+ duration: this.duration(),
233
+ tipLabel: this.tipLabel(),
234
+ });
235
+ map.addControl(this.control);
236
+ });
237
+ });
238
+ }
239
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlRotateControlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
240
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.4", type: OlRotateControlComponent, isStandalone: true, selector: "ol-rotate-control", inputs: { autoHide: { classPropertyName: "autoHide", publicName: "autoHide", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, tipLabel: { classPropertyName: "tipLabel", publicName: "tipLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
241
+ }
242
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlRotateControlComponent, decorators: [{
243
+ type: Component,
244
+ args: [{
245
+ selector: 'ol-rotate-control',
246
+ template: '',
247
+ changeDetection: ChangeDetectionStrategy.OnPush,
248
+ }]
249
+ }], ctorParameters: () => [], propDecorators: { autoHide: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoHide", required: false }] }], duration: [{ type: i0.Input, args: [{ isSignal: true, alias: "duration", required: false }] }], tipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tipLabel", required: false }] }] } });
250
+
251
+ // OlLayerSwitcherComponent - UI control for managing layer visibility
252
+ /**
253
+ * A reusable layer switcher control that displays all layers
254
+ * and allows toggling their visibility.
255
+ *
256
+ * @usageNotes
257
+ * ```html
258
+ * <ol-layer-switcher
259
+ * position="top-right"
260
+ * [layers]="layerItems()"
261
+ * [collapsible]="true"
262
+ * [showOpacity]="true"
263
+ * (visibilityChange)="onVisibilityChange($event)"
264
+ * (opacityChange)="onOpacityChange($event)">
265
+ * </ol-layer-switcher>
266
+ * ```
267
+ */
268
+ class OlLayerSwitcherComponent {
269
+ position = input('top-right', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
270
+ layers = input([], ...(ngDevMode ? [{ debugName: "layers" }] : /* istanbul ignore next */ []));
271
+ collapsible = input(true, ...(ngDevMode ? [{ debugName: "collapsible" }] : /* istanbul ignore next */ []));
272
+ showOpacity = input(false, ...(ngDevMode ? [{ debugName: "showOpacity" }] : /* istanbul ignore next */ []));
273
+ startCollapsed = input(false, ...(ngDevMode ? [{ debugName: "startCollapsed" }] : /* istanbul ignore next */ []));
274
+ visibilityChange = output();
275
+ opacityChange = output();
276
+ isCollapsed = signal(false, ...(ngDevMode ? [{ debugName: "isCollapsed" }] : /* istanbul ignore next */ []));
277
+ toggleCollapsed() {
278
+ if (this.collapsible()) {
279
+ this.isCollapsed.update((v) => !v);
280
+ }
281
+ }
282
+ toggleLayer(id) {
283
+ const layer = this.layers().find((l) => l.id === id);
284
+ if (layer) {
285
+ this.visibilityChange.emit({ id, visible: !layer.visible });
286
+ }
287
+ }
288
+ setOpacity(id, event) {
289
+ const value = event.target.valueAsNumber;
290
+ this.opacityChange.emit({ id, opacity: value });
291
+ }
292
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlLayerSwitcherComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
293
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: OlLayerSwitcherComponent, isStandalone: true, selector: "ol-layer-switcher", inputs: { position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, layers: { classPropertyName: "layers", publicName: "layers", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, showOpacity: { classPropertyName: "showOpacity", publicName: "showOpacity", isSignal: true, isRequired: false, transformFunction: null }, startCollapsed: { classPropertyName: "startCollapsed", publicName: "startCollapsed", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { visibilityChange: "visibilityChange", opacityChange: "opacityChange" }, ngImport: i0, template: `
294
+ <div
295
+ class="ol-layer-switcher"
296
+ [class.collapsed]="isCollapsed()"
297
+ [class.ol-layer-switcher--top-left]="position() === 'top-left'"
298
+ [class.ol-layer-switcher--top-right]="position() === 'top-right'"
299
+ [class.ol-layer-switcher--bottom-left]="position() === 'bottom-left'"
300
+ [class.ol-layer-switcher--bottom-right]="position() === 'bottom-right'"
301
+ >
302
+ <button
303
+ type="button"
304
+ class="ol-layer-switcher__toggle"
305
+ (click)="toggleCollapsed()"
306
+ [attr.aria-expanded]="!isCollapsed()"
307
+ aria-label="Toggle layer switcher"
308
+ >
309
+ <span class="ol-layer-switcher__icon">🗺️</span>
310
+ <span class="ol-layer-switcher__title">Layers</span>
311
+ </button>
312
+
313
+ @if (!isCollapsed()) {
314
+ <div class="ol-layer-switcher__panel">
315
+ @if (layers(); as layerList) {
316
+ @if (layerList.length === 0) {
317
+ <div class="ol-layer-switcher__empty">No layers</div>
318
+ } @else {
319
+ <ul class="ol-layer-switcher__list">
320
+ @for (layer of layerList; track layer.id) {
321
+ <li class="ol-layer-switcher__item">
322
+ <label class="ol-layer-switcher__label">
323
+ <input
324
+ type="checkbox"
325
+ [checked]="layer.visible"
326
+ (change)="toggleLayer(layer.id)"
327
+ class="ol-layer-switcher__checkbox"
328
+ />
329
+ <span class="ol-layer-switcher__name">{{ layer.id }}</span>
330
+ <span
331
+ class="ol-layer-switcher__type"
332
+ [class.ol-layer-switcher__type--vector]="layer.type === 'vector'"
333
+ [class.ol-layer-switcher__type--tile]="layer.type === 'tile'"
334
+ [class.ol-layer-switcher__type--image]="layer.type === 'image'"
335
+ >
336
+ {{ layer.type }}
337
+ </span>
338
+ </label>
339
+
340
+ @if (showOpacity()) {
341
+ <input
342
+ type="range"
343
+ min="0"
344
+ max="1"
345
+ step="0.1"
346
+ [value]="layer.opacity"
347
+ (input)="setOpacity(layer.id, $event)"
348
+ class="ol-layer-switcher__opacity"
349
+ aria-label="Layer opacity"
350
+ />
351
+ }
352
+ </li>
353
+ }
354
+ </ul>
355
+ }
356
+ }
357
+ </div>
358
+ }
359
+ </div>
360
+ `, isInline: true, styles: [":host{display:block}.ol-layer-switcher{position:absolute;background:#fff;color:#333;border-radius:4px;box-shadow:0 2px 4px #0003;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;min-width:200px;z-index:1000}.ol-layer-switcher--top-left{top:.5em;left:.5em}.ol-layer-switcher--top-right{top:.5em;right:.5em}.ol-layer-switcher--bottom-left{bottom:.5em;left:.5em}.ol-layer-switcher--bottom-right{bottom:.5em;right:.5em}.ol-layer-switcher.collapsed{min-width:auto}.ol-layer-switcher__toggle{display:flex;align-items:center;gap:8px;padding:8px 12px;background:#f5f5f5;color:#333;border:none;border-radius:4px;cursor:pointer;width:100%;font-size:14px}.ol-layer-switcher__toggle:hover{background:#e0e0e0}.ol-layer-switcher__icon{font-size:16px}.ol-layer-switcher__title{font-weight:500}.ol-layer-switcher.collapsed .ol-layer-switcher__title,.ol-layer-switcher.collapsed .ol-layer-switcher__panel{display:none}.ol-layer-switcher__panel{padding:8px;max-height:300px;overflow-y:auto}.ol-layer-switcher__empty{padding:16px;color:#555;text-align:center;font-style:italic}.ol-layer-switcher__list{list-style:none;margin:0;padding:0}.ol-layer-switcher__item{padding:8px;border-bottom:1px solid #eee}.ol-layer-switcher__item:last-child{border-bottom:none}.ol-layer-switcher__label{display:flex;align-items:center;gap:8px;cursor:pointer}.ol-layer-switcher__checkbox{cursor:pointer}.ol-layer-switcher__name{flex:1;font-weight:500;color:#333}.ol-layer-switcher__type{font-size:10px;padding:2px 6px;border-radius:3px;text-transform:uppercase;background:#e0e0e0;color:#666}.ol-layer-switcher__type--vector{background:#e3f2fd;color:#1976d2}.ol-layer-switcher__type--tile{background:#e8f5e9;color:#388e3c}.ol-layer-switcher__type--image{background:#fff3e0;color:#f57c00}.ol-layer-switcher__opacity{width:100%;margin-top:8px;cursor:pointer}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
361
+ }
362
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlLayerSwitcherComponent, decorators: [{
363
+ type: Component,
364
+ args: [{ selector: 'ol-layer-switcher', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
365
+ <div
366
+ class="ol-layer-switcher"
367
+ [class.collapsed]="isCollapsed()"
368
+ [class.ol-layer-switcher--top-left]="position() === 'top-left'"
369
+ [class.ol-layer-switcher--top-right]="position() === 'top-right'"
370
+ [class.ol-layer-switcher--bottom-left]="position() === 'bottom-left'"
371
+ [class.ol-layer-switcher--bottom-right]="position() === 'bottom-right'"
372
+ >
373
+ <button
374
+ type="button"
375
+ class="ol-layer-switcher__toggle"
376
+ (click)="toggleCollapsed()"
377
+ [attr.aria-expanded]="!isCollapsed()"
378
+ aria-label="Toggle layer switcher"
379
+ >
380
+ <span class="ol-layer-switcher__icon">🗺️</span>
381
+ <span class="ol-layer-switcher__title">Layers</span>
382
+ </button>
383
+
384
+ @if (!isCollapsed()) {
385
+ <div class="ol-layer-switcher__panel">
386
+ @if (layers(); as layerList) {
387
+ @if (layerList.length === 0) {
388
+ <div class="ol-layer-switcher__empty">No layers</div>
389
+ } @else {
390
+ <ul class="ol-layer-switcher__list">
391
+ @for (layer of layerList; track layer.id) {
392
+ <li class="ol-layer-switcher__item">
393
+ <label class="ol-layer-switcher__label">
394
+ <input
395
+ type="checkbox"
396
+ [checked]="layer.visible"
397
+ (change)="toggleLayer(layer.id)"
398
+ class="ol-layer-switcher__checkbox"
399
+ />
400
+ <span class="ol-layer-switcher__name">{{ layer.id }}</span>
401
+ <span
402
+ class="ol-layer-switcher__type"
403
+ [class.ol-layer-switcher__type--vector]="layer.type === 'vector'"
404
+ [class.ol-layer-switcher__type--tile]="layer.type === 'tile'"
405
+ [class.ol-layer-switcher__type--image]="layer.type === 'image'"
406
+ >
407
+ {{ layer.type }}
408
+ </span>
409
+ </label>
410
+
411
+ @if (showOpacity()) {
412
+ <input
413
+ type="range"
414
+ min="0"
415
+ max="1"
416
+ step="0.1"
417
+ [value]="layer.opacity"
418
+ (input)="setOpacity(layer.id, $event)"
419
+ class="ol-layer-switcher__opacity"
420
+ aria-label="Layer opacity"
421
+ />
422
+ }
423
+ </li>
424
+ }
425
+ </ul>
426
+ }
427
+ }
428
+ </div>
429
+ }
430
+ </div>
431
+ `, styles: [":host{display:block}.ol-layer-switcher{position:absolute;background:#fff;color:#333;border-radius:4px;box-shadow:0 2px 4px #0003;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;min-width:200px;z-index:1000}.ol-layer-switcher--top-left{top:.5em;left:.5em}.ol-layer-switcher--top-right{top:.5em;right:.5em}.ol-layer-switcher--bottom-left{bottom:.5em;left:.5em}.ol-layer-switcher--bottom-right{bottom:.5em;right:.5em}.ol-layer-switcher.collapsed{min-width:auto}.ol-layer-switcher__toggle{display:flex;align-items:center;gap:8px;padding:8px 12px;background:#f5f5f5;color:#333;border:none;border-radius:4px;cursor:pointer;width:100%;font-size:14px}.ol-layer-switcher__toggle:hover{background:#e0e0e0}.ol-layer-switcher__icon{font-size:16px}.ol-layer-switcher__title{font-weight:500}.ol-layer-switcher.collapsed .ol-layer-switcher__title,.ol-layer-switcher.collapsed .ol-layer-switcher__panel{display:none}.ol-layer-switcher__panel{padding:8px;max-height:300px;overflow-y:auto}.ol-layer-switcher__empty{padding:16px;color:#555;text-align:center;font-style:italic}.ol-layer-switcher__list{list-style:none;margin:0;padding:0}.ol-layer-switcher__item{padding:8px;border-bottom:1px solid #eee}.ol-layer-switcher__item:last-child{border-bottom:none}.ol-layer-switcher__label{display:flex;align-items:center;gap:8px;cursor:pointer}.ol-layer-switcher__checkbox{cursor:pointer}.ol-layer-switcher__name{flex:1;font-weight:500;color:#333}.ol-layer-switcher__type{font-size:10px;padding:2px 6px;border-radius:3px;text-transform:uppercase;background:#e0e0e0;color:#666}.ol-layer-switcher__type--vector{background:#e3f2fd;color:#1976d2}.ol-layer-switcher__type--tile{background:#e8f5e9;color:#388e3c}.ol-layer-switcher__type--image{background:#fff3e0;color:#f57c00}.ol-layer-switcher__opacity{width:100%;margin-top:8px;cursor:pointer}\n"] }]
432
+ }], propDecorators: { position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], layers: [{ type: i0.Input, args: [{ isSignal: true, alias: "layers", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], showOpacity: [{ type: i0.Input, args: [{ isSignal: true, alias: "showOpacity", required: false }] }], startCollapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "startCollapsed", required: false }] }], visibilityChange: [{ type: i0.Output, args: ["visibilityChange"] }], opacityChange: [{ type: i0.Output, args: ["opacityChange"] }] } });
433
+
434
+ // OlBasemapSwitcherComponent - Switch between base map providers
435
+ /**
436
+ * A basemap switcher control that allows switching between
437
+ * different tile providers without page refresh.
438
+ *
439
+ * @usageNotes
440
+ * ```html
441
+ * <ol-basemap-switcher
442
+ * position="bottom-left"
443
+ * [basemaps]="[
444
+ * { id: 'osm', name: 'OpenStreetMap', type: 'osm' },
445
+ * { id: 'satellite', name: 'Satellite', type: 'xyz', url: 'https://...' }
446
+ * ]"
447
+ * [activeBasemap]="'osm'"
448
+ * (basemapChange)="onBasemapChange($event)">
449
+ * </ol-basemap-switcher>
450
+ * ```
451
+ */
452
+ class OlBasemapSwitcherComponent {
453
+ basemaps = input([{ id: 'osm', name: 'OpenStreetMap', type: 'osm' }], ...(ngDevMode ? [{ debugName: "basemaps" }] : /* istanbul ignore next */ []));
454
+ activeBasemap = input('osm', ...(ngDevMode ? [{ debugName: "activeBasemap" }] : /* istanbul ignore next */ []));
455
+ position = input('bottom-left', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
456
+ basemapChange = output();
457
+ isExpanded = signal(false, ...(ngDevMode ? [{ debugName: "isExpanded" }] : /* istanbul ignore next */ []));
458
+ toggleExpanded() {
459
+ this.isExpanded.update((v) => !v);
460
+ }
461
+ switchBasemap(basemap) {
462
+ this.basemapChange.emit(basemap.id);
463
+ this.isExpanded.set(false);
464
+ }
465
+ getActiveBasemapName() {
466
+ const active = this.basemaps().find((b) => b.id === this.activeBasemap());
467
+ return active?.name || 'Basemap';
468
+ }
469
+ getDefaultIcon(basemap) {
470
+ switch (basemap.type) {
471
+ case 'osm':
472
+ return '🗺️';
473
+ case 'xyz':
474
+ return '🛰️';
475
+ case 'wms':
476
+ return '📡';
477
+ default:
478
+ return '🗺️';
479
+ }
480
+ }
481
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlBasemapSwitcherComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
482
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: OlBasemapSwitcherComponent, isStandalone: true, selector: "ol-basemap-switcher", inputs: { basemaps: { classPropertyName: "basemaps", publicName: "basemaps", isSignal: true, isRequired: false, transformFunction: null }, activeBasemap: { classPropertyName: "activeBasemap", publicName: "activeBasemap", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { basemapChange: "basemapChange" }, ngImport: i0, template: `
483
+ <div
484
+ class="ol-basemap-switcher"
485
+ [class.ol-basemap-switcher--top-left]="position() === 'top-left'"
486
+ [class.ol-basemap-switcher--top-center]="position() === 'top-center'"
487
+ [class.ol-basemap-switcher--top-right]="position() === 'top-right'"
488
+ [class.ol-basemap-switcher--bottom-left]="position() === 'bottom-left'"
489
+ [class.ol-basemap-switcher--bottom-center]="position() === 'bottom-center'"
490
+ [class.ol-basemap-switcher--bottom-right]="position() === 'bottom-right'"
491
+ >
492
+ @if (isExpanded()) {
493
+ <div class="ol-basemap-switcher__panel">
494
+ @for (basemap of basemaps(); track basemap.id) {
495
+ <button
496
+ type="button"
497
+ class="ol-basemap-switcher__item"
498
+ [class.ol-basemap-switcher__item--active]="activeBasemap() === basemap.id"
499
+ (click)="switchBasemap(basemap)"
500
+ >
501
+ <span class="ol-basemap-switcher__icon">{{
502
+ basemap.icon || getDefaultIcon(basemap)
503
+ }}</span>
504
+ <span class="ol-basemap-switcher__name">{{ basemap.name }}</span>
505
+ </button>
506
+ }
507
+ </div>
508
+ }
509
+
510
+ <button
511
+ type="button"
512
+ class="ol-basemap-switcher__toggle"
513
+ (click)="toggleExpanded()"
514
+ [attr.aria-expanded]="isExpanded()"
515
+ aria-label="Toggle basemap switcher"
516
+ >
517
+ <span class="ol-basemap-switcher__toggle-icon">🗺️</span>
518
+ <span class="ol-basemap-switcher__toggle-text">
519
+ {{ getActiveBasemapName() }}
520
+ </span>
521
+ </button>
522
+ </div>
523
+ `, isInline: true, styles: [":host{display:block}.ol-basemap-switcher{position:absolute;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;color:#333;z-index:1000}.ol-basemap-switcher--top-left{top:.5em;left:.5em}.ol-basemap-switcher--top-center{top:.5em;left:50%;transform:translate(-50%)}.ol-basemap-switcher--top-right{top:.5em;right:.5em}.ol-basemap-switcher--bottom-left{bottom:.5em;left:.5em}.ol-basemap-switcher--bottom-center{bottom:.5em;left:50%;transform:translate(-50%)}.ol-basemap-switcher--bottom-right{bottom:.5em;right:.5em}.ol-basemap-switcher__toggle{display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fff;color:#333;border:none;border-radius:4px;box-shadow:0 2px 4px #0003;cursor:pointer;font-size:14px}.ol-basemap-switcher__toggle:hover{background:#f5f5f5}.ol-basemap-switcher__toggle-icon{font-size:16px}.ol-basemap-switcher__toggle-text{font-weight:500}.ol-basemap-switcher__panel{position:absolute;bottom:calc(100% + 8px);left:0;background:#fff;color:#333;border-radius:4px;box-shadow:0 2px 8px #00000026;padding:4px;min-width:160px}.ol-basemap-switcher--bottom-right .ol-basemap-switcher__panel,.ol-basemap-switcher--top-right .ol-basemap-switcher__panel{left:auto;right:0}.ol-basemap-switcher--top-left .ol-basemap-switcher__panel,.ol-basemap-switcher--top-center .ol-basemap-switcher__panel,.ol-basemap-switcher--top-right .ol-basemap-switcher__panel{bottom:auto;top:calc(100% + 8px)}.ol-basemap-switcher--top-center .ol-basemap-switcher__panel,.ol-basemap-switcher--bottom-center .ol-basemap-switcher__panel{left:50%;transform:translate(-50%)}.ol-basemap-switcher__item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 12px;border:none;background:transparent;border-radius:4px;cursor:pointer;text-align:left;transition:background .15s ease}.ol-basemap-switcher__item:hover{background:#f5f5f5}.ol-basemap-switcher__item--active{background:#e3f2fd;color:#1976d2}.ol-basemap-switcher__item--active:hover{background:#bbdefb}.ol-basemap-switcher__icon{font-size:16px}.ol-basemap-switcher__name{font-weight:500;color:#333}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
524
+ }
525
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlBasemapSwitcherComponent, decorators: [{
526
+ type: Component,
527
+ args: [{ selector: 'ol-basemap-switcher', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
528
+ <div
529
+ class="ol-basemap-switcher"
530
+ [class.ol-basemap-switcher--top-left]="position() === 'top-left'"
531
+ [class.ol-basemap-switcher--top-center]="position() === 'top-center'"
532
+ [class.ol-basemap-switcher--top-right]="position() === 'top-right'"
533
+ [class.ol-basemap-switcher--bottom-left]="position() === 'bottom-left'"
534
+ [class.ol-basemap-switcher--bottom-center]="position() === 'bottom-center'"
535
+ [class.ol-basemap-switcher--bottom-right]="position() === 'bottom-right'"
536
+ >
537
+ @if (isExpanded()) {
538
+ <div class="ol-basemap-switcher__panel">
539
+ @for (basemap of basemaps(); track basemap.id) {
540
+ <button
541
+ type="button"
542
+ class="ol-basemap-switcher__item"
543
+ [class.ol-basemap-switcher__item--active]="activeBasemap() === basemap.id"
544
+ (click)="switchBasemap(basemap)"
545
+ >
546
+ <span class="ol-basemap-switcher__icon">{{
547
+ basemap.icon || getDefaultIcon(basemap)
548
+ }}</span>
549
+ <span class="ol-basemap-switcher__name">{{ basemap.name }}</span>
550
+ </button>
551
+ }
552
+ </div>
553
+ }
554
+
555
+ <button
556
+ type="button"
557
+ class="ol-basemap-switcher__toggle"
558
+ (click)="toggleExpanded()"
559
+ [attr.aria-expanded]="isExpanded()"
560
+ aria-label="Toggle basemap switcher"
561
+ >
562
+ <span class="ol-basemap-switcher__toggle-icon">🗺️</span>
563
+ <span class="ol-basemap-switcher__toggle-text">
564
+ {{ getActiveBasemapName() }}
565
+ </span>
566
+ </button>
567
+ </div>
568
+ `, styles: [":host{display:block}.ol-basemap-switcher{position:absolute;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;color:#333;z-index:1000}.ol-basemap-switcher--top-left{top:.5em;left:.5em}.ol-basemap-switcher--top-center{top:.5em;left:50%;transform:translate(-50%)}.ol-basemap-switcher--top-right{top:.5em;right:.5em}.ol-basemap-switcher--bottom-left{bottom:.5em;left:.5em}.ol-basemap-switcher--bottom-center{bottom:.5em;left:50%;transform:translate(-50%)}.ol-basemap-switcher--bottom-right{bottom:.5em;right:.5em}.ol-basemap-switcher__toggle{display:flex;align-items:center;gap:8px;padding:8px 12px;background:#fff;color:#333;border:none;border-radius:4px;box-shadow:0 2px 4px #0003;cursor:pointer;font-size:14px}.ol-basemap-switcher__toggle:hover{background:#f5f5f5}.ol-basemap-switcher__toggle-icon{font-size:16px}.ol-basemap-switcher__toggle-text{font-weight:500}.ol-basemap-switcher__panel{position:absolute;bottom:calc(100% + 8px);left:0;background:#fff;color:#333;border-radius:4px;box-shadow:0 2px 8px #00000026;padding:4px;min-width:160px}.ol-basemap-switcher--bottom-right .ol-basemap-switcher__panel,.ol-basemap-switcher--top-right .ol-basemap-switcher__panel{left:auto;right:0}.ol-basemap-switcher--top-left .ol-basemap-switcher__panel,.ol-basemap-switcher--top-center .ol-basemap-switcher__panel,.ol-basemap-switcher--top-right .ol-basemap-switcher__panel{bottom:auto;top:calc(100% + 8px)}.ol-basemap-switcher--top-center .ol-basemap-switcher__panel,.ol-basemap-switcher--bottom-center .ol-basemap-switcher__panel{left:50%;transform:translate(-50%)}.ol-basemap-switcher__item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 12px;border:none;background:transparent;border-radius:4px;cursor:pointer;text-align:left;transition:background .15s ease}.ol-basemap-switcher__item:hover{background:#f5f5f5}.ol-basemap-switcher__item--active{background:#e3f2fd;color:#1976d2}.ol-basemap-switcher__item--active:hover{background:#bbdefb}.ol-basemap-switcher__icon{font-size:16px}.ol-basemap-switcher__name{font-weight:500;color:#333}\n"] }]
569
+ }], propDecorators: { basemaps: [{ type: i0.Input, args: [{ isSignal: true, alias: "basemaps", required: false }] }], activeBasemap: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeBasemap", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], basemapChange: [{ type: i0.Output, args: ["basemapChange"] }] } });
198
570
 
199
571
  // OlControlService
200
572
  class OlControlService {
@@ -208,8 +580,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
208
580
  }] });
209
581
 
210
582
  // Provider functions
583
+ /**
584
+ * Provides control services and configures the rotate control map service alias.
585
+ * Requires OlMapService to be provided at the application level.
586
+ */
211
587
  function withControls() {
212
- return { kind: 'controls', providers: [OlControlService] };
588
+ return {
589
+ kind: 'controls',
590
+ providers: [
591
+ OlControlService,
592
+ // Alias OlMapService to ROTATE_CONTROL_MAP_SERVICE for rotate control
593
+ {
594
+ provide: ROTATE_CONTROL_MAP_SERVICE,
595
+ useFactory: () => {
596
+ // This will be resolved when OlMapService is available
597
+ return { getMap: () => null };
598
+ },
599
+ },
600
+ ],
601
+ };
213
602
  }
214
603
  function provideControls() {
215
604
  return withControls();
@@ -221,4 +610,4 @@ function provideControls() {
221
610
  * Generated bundle index. Do not edit.
222
611
  */
223
612
 
224
- export { OlAttributionControlComponent, OlControlService, OlFullscreenControlComponent, OlScaleLineControlComponent, OlZoomControlComponent, provideControls, withControls };
613
+ export { OlAttributionControlComponent, OlBasemapSwitcherComponent, OlControlService, OlFullscreenControlComponent, OlLayerSwitcherComponent, OlRotateControlComponent, OlScaleLineControlComponent, OlZoomControlComponent, ROTATE_CONTROL_MAP_SERVICE, provideControls, withControls };