inviton-powerduck 0.0.331 → 0.0.332
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/app/powerduck-system-resources.ts +1 -0
- package/common/utils/file-helper.ts +19 -3
- package/common/utils/upload-image-helper.ts +8 -1
- package/components/input/gps-bounding-box.tsx +5 -0
- package/components/input/gps-input.tsx +7 -0
- package/components/input/numeric-input.tsx +81 -0
- package/components/open-street-map/open-street-map.tsx +159 -38
- package/package.json +1 -1
|
@@ -3,16 +3,32 @@ import PowerduckState from '../../app/powerduck-state';
|
|
|
3
3
|
import { AppHttpProvider } from '../api-http';
|
|
4
4
|
import { DomainHelper } from './domain-helper';
|
|
5
5
|
|
|
6
|
+
const ABSOLUTE_URL_RE = /^https?:\/\//i;
|
|
7
|
+
const LEADING_SLASH_ABSOLUTE_RE = /^\/https?:\/\//i;
|
|
8
|
+
|
|
6
9
|
export default class FileHelper {
|
|
7
10
|
static getFullPath(file: Photo) {
|
|
8
|
-
if (file == null) {
|
|
11
|
+
if (file == null || file.path == null) {
|
|
9
12
|
return null;
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
const rawPath = file.path.trim();
|
|
16
|
+
|
|
17
|
+
// 1. Already-absolute URL (Bunny CDN, S3, data:, protocol-relative) — return as-is.
|
|
18
|
+
if (ABSOLUTE_URL_RE.test(rawPath) || rawPath.startsWith('//') || rawPath.startsWith('data:')) {
|
|
19
|
+
return rawPath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 2. Stray leading-slash before scheme ("/https://..."): strip the slash.
|
|
23
|
+
if (LEADING_SLASH_ABSOLUTE_RE.test(rawPath)) {
|
|
24
|
+
return rawPath.slice(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 3. Legacy relative path (e.g. "/photos/resorts/file.png") — resolve against backend domain.
|
|
12
28
|
if (AppHttpProvider.enforceDomain == '/') {
|
|
13
|
-
return PowerduckState.getCdnPath() + PowerduckState.getFilesPath() +
|
|
29
|
+
return PowerduckState.getCdnPath() + PowerduckState.getFilesPath() + rawPath;
|
|
14
30
|
}
|
|
15
31
|
|
|
16
|
-
return DomainHelper.getDomainFromUrl(AppHttpProvider.enforceDomain, true) + PowerduckState.getFilesPath() +
|
|
32
|
+
return DomainHelper.getDomainFromUrl(AppHttpProvider.enforceDomain, true) + PowerduckState.getFilesPath() + rawPath;
|
|
17
33
|
}
|
|
18
34
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ImageResponse, OnUploadImageResponse } from '../../data/image';
|
|
2
2
|
import { globalState } from '../../app/global-state';
|
|
3
3
|
import PowerduckState from '../../app/powerduck-state';
|
|
4
|
+
import NotificationProvider from '../../components/ui/notification';
|
|
4
5
|
import { AppHttpProvider } from '../api-http';
|
|
5
6
|
import { BrowserImageCompression } from './broswer-image-compression';
|
|
6
7
|
import { isNullOrEmpty } from './is-null-or-empty';
|
|
@@ -25,7 +26,7 @@ export class UploadImageHelper {
|
|
|
25
26
|
disableCompression: boolean = false,
|
|
26
27
|
compressionMaxMb: number = 0.5,
|
|
27
28
|
url?: string,
|
|
28
|
-
): Promise<ImageResponse> {
|
|
29
|
+
): Promise<ImageResponse | null> {
|
|
29
30
|
if (!(disableCompression || file.type == 'image/svg+xml')) {
|
|
30
31
|
file = await BrowserImageCompression.compress(file, {
|
|
31
32
|
maxSizeMB: compressionMaxMb,
|
|
@@ -49,6 +50,12 @@ export class UploadImageHelper {
|
|
|
49
50
|
credentials: UploadImageHelperConfig.includeCredentials ? PowerduckState.getIsDebugMode() ? 'include' : 'same-origin' : 'omit',
|
|
50
51
|
});
|
|
51
52
|
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
NotificationProvider.showErrorMessage(PowerduckState.getResourceValue('uploadFailed'));
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
return (await response.json()) as ImageResponse;
|
|
53
60
|
}
|
|
54
61
|
|
|
@@ -32,6 +32,10 @@ class GpsBoundingBoxInputComponent extends TsxComponent<GpsBoundingBoxInputArgs>
|
|
|
32
32
|
|
|
33
33
|
onChange(field: keyof BoundingBox, value: number) {
|
|
34
34
|
// QA_AT-11: align with `GpsInput` and round to 6 decimals (~11 cm).
|
|
35
|
+
// QA_AT-24: NumericInput now string-truncates while typing (via
|
|
36
|
+
// maxDecimals={6} in renderInput). This toFixed(6) round remains as a
|
|
37
|
+
// defence-in-depth net for non-input paths — paste, programmatic
|
|
38
|
+
// bbox prefills from country / GPS centroid recompute, etc.
|
|
35
39
|
if (value) {
|
|
36
40
|
this.bBoxModel[field] = parseFloat(value.toFixed(6));
|
|
37
41
|
} else {
|
|
@@ -58,6 +62,7 @@ class GpsBoundingBoxInputComponent extends TsxComponent<GpsBoundingBoxInputArgs>
|
|
|
58
62
|
label={label}
|
|
59
63
|
value={value}
|
|
60
64
|
step={0.000001}
|
|
65
|
+
maxDecimals={6}
|
|
61
66
|
mode={NumericInputMode.Clasic}
|
|
62
67
|
showClearValueButton={showClearValueButton}
|
|
63
68
|
validationState={validationState}
|
|
@@ -61,12 +61,17 @@ class GpsInputComponent extends TsxComponent<GpsInputArgs> implements GpsInputAr
|
|
|
61
61
|
label={null}
|
|
62
62
|
value={this.gpsModel.latitude}
|
|
63
63
|
step={0.000001}
|
|
64
|
+
maxDecimals={6}
|
|
64
65
|
mode={NumericInputMode.Clasic}
|
|
65
66
|
showClearValueButton={this.showClearValueButton}
|
|
66
67
|
validationState={this.validationStateLatitude}
|
|
67
68
|
disabled={this.disabled}
|
|
68
69
|
changed={(e) => {
|
|
69
70
|
if (e) {
|
|
71
|
+
// QA_AT-24: NumericInput now string-truncates while typing
|
|
72
|
+
// (via maxDecimals={6}). The toFixed(6) round here remains as
|
|
73
|
+
// a defence-in-depth net for non-input paths — map
|
|
74
|
+
// contextmenu click, programmatic centroid prefills, paste.
|
|
70
75
|
this.gpsModel.latitude = parseFloat(e.toFixed(6));
|
|
71
76
|
} else {
|
|
72
77
|
this.gpsModel.latitude = null;
|
|
@@ -81,12 +86,14 @@ class GpsInputComponent extends TsxComponent<GpsInputArgs> implements GpsInputAr
|
|
|
81
86
|
label={null}
|
|
82
87
|
value={this.gpsModel.longitude}
|
|
83
88
|
step={0.000001}
|
|
89
|
+
maxDecimals={6}
|
|
84
90
|
mode={NumericInputMode.Clasic}
|
|
85
91
|
showClearValueButton={this.showClearValueButton}
|
|
86
92
|
validationState={this.validationStateLongitude}
|
|
87
93
|
disabled={this.disabled}
|
|
88
94
|
changed={(e) => {
|
|
89
95
|
if (e) {
|
|
96
|
+
// QA_AT-24: see latitude comment — same defence-in-depth net.
|
|
90
97
|
this.gpsModel.longitude = parseFloat(e.toFixed(6));
|
|
91
98
|
} else {
|
|
92
99
|
this.gpsModel.longitude = null;
|
|
@@ -23,6 +23,26 @@ interface NumericInputArgs extends Omit<FormItemWrapperArgs, 'errorId'> {
|
|
|
23
23
|
updateMode?: 'input' | 'change';
|
|
24
24
|
placeholder?: string;
|
|
25
25
|
decimalsAlwaysVisible?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* QA_AT-24: when set (Classic mode only), the input clamps the visible
|
|
28
|
+
* value to at most this many decimal places AS THE USER TYPES — not only
|
|
29
|
+
* on blur / save. Excess decimal characters are stripped via string
|
|
30
|
+
* truncation (NOT numeric rounding) so digits the operator typed are
|
|
31
|
+
* never silently rewritten; the DOM `input.value` is rewritten in place.
|
|
32
|
+
* Intermediate states like `12.` are preserved so the operator can
|
|
33
|
+
* continue typing.
|
|
34
|
+
*
|
|
35
|
+
* The `changed` callback respects `updateMode`: under the default
|
|
36
|
+
* (change-on-blur) it is NOT fired per-keystroke even when truncation
|
|
37
|
+
* happens — the trailing native `change` on blur reads the truncated DOM
|
|
38
|
+
* value and propagates it. Callers that want per-keystroke updates must
|
|
39
|
+
* opt in with `updateMode='input'`.
|
|
40
|
+
*
|
|
41
|
+
* Undefined = legacy unbounded-precision behaviour preserved for all
|
|
42
|
+
* existing callers. Spinner mode ignores this prop (Spinner editor
|
|
43
|
+
* already enforces decimals via Intl.NumberFormat on blur).
|
|
44
|
+
*/
|
|
45
|
+
maxDecimals?: number;
|
|
26
46
|
changed: (newValue: number) => void;
|
|
27
47
|
mode?: NumericInputMode;
|
|
28
48
|
disabled?: boolean;
|
|
@@ -52,6 +72,7 @@ class NumericInputComponent extends TsxComponent<NumericInputArgs> implements Nu
|
|
|
52
72
|
@Prop() appendClicked: () => void;
|
|
53
73
|
@Prop() prependClicked: () => void;
|
|
54
74
|
@Prop() decimalsAlwaysVisible!: boolean;
|
|
75
|
+
@Prop() maxDecimals?: number;
|
|
55
76
|
@Prop() mode: NumericInputMode;
|
|
56
77
|
@Prop() updateMode?: 'input' | 'change';
|
|
57
78
|
@Prop() disabled?: boolean;
|
|
@@ -92,6 +113,50 @@ class NumericInputComponent extends TsxComponent<NumericInputArgs> implements Nu
|
|
|
92
113
|
}
|
|
93
114
|
}
|
|
94
115
|
|
|
116
|
+
/**
|
|
117
|
+
* QA_AT-24: when `maxDecimals` is set and the typed value carries more
|
|
118
|
+
* decimal places than allowed, rewrite the DOM `input.value` to the
|
|
119
|
+
* truncated representation. Returns the truncated numeric value, or
|
|
120
|
+
* `null` when no truncation happened or the input is not yet a complete
|
|
121
|
+
* number — e.g. the intermediate state `12.` keeps its trailing dot so
|
|
122
|
+
* the operator can continue typing.
|
|
123
|
+
*
|
|
124
|
+
* Non-destructive for intermediate states: typing `12.` leaves `12.` in
|
|
125
|
+
* the DOM; only EXCESS decimal characters past `maxDecimals` are
|
|
126
|
+
* stripped. Uses string truncation (NOT numeric rounding) so digits the
|
|
127
|
+
* operator already typed are never silently rewritten — the save-side
|
|
128
|
+
* `parseFloat(e.toFixed(6))` in GpsInput / GpsBoundingBoxInput remains as
|
|
129
|
+
* a defence-in-depth rounding net for non-input paths.
|
|
130
|
+
*/
|
|
131
|
+
private clampToMaxDecimals(inputEl: HTMLInputElement): number | null {
|
|
132
|
+
if (this.maxDecimals == null || this.maxDecimals < 0) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const raw = inputEl.value;
|
|
137
|
+
if (raw == null || raw === '') {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Preserve trailing "." and partial inputs like "12." — they have a
|
|
142
|
+
// length-0 decimal substring which is <= cap.
|
|
143
|
+
const dotIndex = raw.indexOf('.');
|
|
144
|
+
if (dotIndex < 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const decimalPart = raw.slice(dotIndex + 1);
|
|
149
|
+
if (decimalPart.length <= this.maxDecimals) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const truncatedString = raw.slice(0, dotIndex + 1 + this.maxDecimals);
|
|
154
|
+
inputEl.value = truncatedString;
|
|
155
|
+
const parsed = parseFloat(truncatedString);
|
|
156
|
+
|
|
157
|
+
return isNaN(parsed) ? null : parsed;
|
|
158
|
+
}
|
|
159
|
+
|
|
95
160
|
getMode(): NumericInputMode {
|
|
96
161
|
if (this.mode != null) {
|
|
97
162
|
return this.mode;
|
|
@@ -150,6 +215,22 @@ class NumericInputComponent extends TsxComponent<NumericInputArgs> implements Nu
|
|
|
150
215
|
value={inputValue}
|
|
151
216
|
onChange={e => this.raiseChangeEvent(e)}
|
|
152
217
|
onInput={(e) => {
|
|
218
|
+
// QA_AT-24: input-time decimal clamp. Always trims the DOM
|
|
219
|
+
// `input.value` when `maxDecimals` is exceeded so the operator
|
|
220
|
+
// never sees more digits than allowed; the `changed` callback
|
|
221
|
+
// however respects `updateMode` — under the default
|
|
222
|
+
// change-on-blur contract the trailing native `change` reads
|
|
223
|
+
// the truncated DOM value and propagates it, so we don't fire
|
|
224
|
+
// per-keystroke (which previously caused mid-typing reactive
|
|
225
|
+
// storms on `gpsModel.latitude` → map re-centering →
|
|
226
|
+
// validation re-runs).
|
|
227
|
+
if (this.maxDecimals != null && this.maxDecimals >= 0) {
|
|
228
|
+
const truncated = this.clampToMaxDecimals(e.target as HTMLInputElement);
|
|
229
|
+
if (truncated != null && this.changed != null && this.updateMode == 'input') {
|
|
230
|
+
this.changed(truncated);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
153
234
|
if (this.updateMode == 'input') {
|
|
154
235
|
this.handleClassicInput(e);
|
|
155
236
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type BoundingBox from '../../data/bounding-box';
|
|
1
2
|
import { LGeoJson, LLayerGroup, LMap, LMarker, LPolygon, LTileLayer } from '@vue-leaflet/vue-leaflet';
|
|
2
3
|
import L from 'leaflet';
|
|
3
4
|
import * as LCluster from 'leaflet.markercluster';
|
|
@@ -5,7 +6,6 @@ import { Prop, toNative, Watch } from 'vue-facing-decorator';
|
|
|
5
6
|
import TsxComponent, { Component } from '../../app/vuetsx';
|
|
6
7
|
import { isNullOrEmpty } from '../../common/utils/is-null-or-empty';
|
|
7
8
|
import { ModalUtils } from '../modal/modal-utils';
|
|
8
|
-
import BoundingBox from '../../data/bounding-box';
|
|
9
9
|
import iconUrl from './img/marker-icon.png';
|
|
10
10
|
import GestureHandling from './ts/gesture-handling/gesture-handling';
|
|
11
11
|
import 'leaflet/dist/leaflet.css';
|
|
@@ -77,7 +77,7 @@ interface OpenStreetMapArgs {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export class OpenStreetMapHelper {
|
|
80
|
-
static getMarkerIcon
|
|
80
|
+
static getMarkerIcon(): L.Icon<L.IconOptions> {
|
|
81
81
|
return L.icon({
|
|
82
82
|
iconUrl,
|
|
83
83
|
iconAnchor: [
|
|
@@ -135,8 +135,21 @@ export class OpenStreetMapComponent extends TsxComponent<OpenStreetMapArgs> impl
|
|
|
135
135
|
@Prop() initComplete?: (map: any, mapHandler: any) => void;
|
|
136
136
|
leafletIcon: any = null;
|
|
137
137
|
markerClusters: LCluster.MarkerClusterGroup = null;
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
/**
|
|
139
|
+
* QA_AT-21: self-heal observer. Leaflet caches container size at mount
|
|
140
|
+
* (`L.Map._size`); when the host element starts at 0×0 (hidden tab pane,
|
|
141
|
+
* collapsed sidebar, dismissed modal, deferred reveal) only one tile in
|
|
142
|
+
* the top-left renders and `containerPointToLatLng` returns drifted
|
|
143
|
+
* coordinates for right-click pin moves. `ResizeObserver` fires once the
|
|
144
|
+
* element gets its real size and recalls `invalidateSize()` — generic
|
|
145
|
+
* mechanism that benefits every consumer (admin location/resort modals,
|
|
146
|
+
* shop search-results-map-view, product-picker-dropdown-map, ...).
|
|
147
|
+
*/
|
|
148
|
+
private _resizeObserver: ResizeObserver | null = null;
|
|
149
|
+
private _lastObservedWidth: number = 0;
|
|
150
|
+
private _pendingResizeRaf: number | null = null;
|
|
151
|
+
|
|
152
|
+
mounted() {
|
|
140
153
|
this.initIcon();
|
|
141
154
|
this.waitForLeaflet().then(() => {
|
|
142
155
|
ModalUtils.handleModalMountDelay(this, () => {
|
|
@@ -149,21 +162,103 @@ export class OpenStreetMapComponent extends TsxComponent<OpenStreetMapArgs> impl
|
|
|
149
162
|
this.initComplete(this.getLeaflet(), this.getMap());
|
|
150
163
|
});
|
|
151
164
|
}
|
|
165
|
+
|
|
166
|
+
// QA_AT-21: attach AFTER the initial invalidateSize so the
|
|
167
|
+
// first reading we cache reflects the real mounted size and
|
|
168
|
+
// the observer fires only on subsequent meaningful resizes.
|
|
169
|
+
this.attachResizeObserver();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
beforeUnmount() {
|
|
175
|
+
// QA_AT-21: clean up the observer so detached components don't keep
|
|
176
|
+
// calling `invalidateSize()` on a torn-down map instance.
|
|
177
|
+
if (this._resizeObserver != null) {
|
|
178
|
+
this._resizeObserver.disconnect();
|
|
179
|
+
this._resizeObserver = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (this._pendingResizeRaf != null && typeof (globalThis as any).cancelAnimationFrame === 'function') {
|
|
183
|
+
(globalThis as any).cancelAnimationFrame(this._pendingResizeRaf);
|
|
184
|
+
this._pendingResizeRaf = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private attachResizeObserver() {
|
|
189
|
+
const el = this.$el as HTMLElement | null;
|
|
190
|
+
if (el == null) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Guard for SSR + jsdom (`ResizeObserver` only exists in real browsers;
|
|
195
|
+
// Chromium 64+ / Firefox 69+).
|
|
196
|
+
if (typeof globalThis === 'undefined' || typeof (globalThis as any).ResizeObserver !== 'function') {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const RO = (globalThis as any).ResizeObserver as typeof ResizeObserver;
|
|
201
|
+
this._lastObservedWidth = el.getBoundingClientRect().width || 0;
|
|
202
|
+
this._resizeObserver = new RO((entries) => {
|
|
203
|
+
const entry = entries[0];
|
|
204
|
+
if (entry == null) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const width = entry.contentRect?.width ?? 0;
|
|
209
|
+
// Skip no-op resizes — `invalidateSize()` is cheap but schedules a
|
|
210
|
+
// re-render; gate on width-delta > 1 px to prevent feedback loops.
|
|
211
|
+
if (Math.abs(width - this._lastObservedWidth) < 1) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this._lastObservedWidth = width;
|
|
216
|
+
// Only run once the container has a real size so the meaningful
|
|
217
|
+
// first invalidate happens AFTER the container becomes visible.
|
|
218
|
+
if (width <= 0) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// QA_AT-21: rAF-coalesce rapid RO fires (window-resize drag,
|
|
223
|
+
// sidebar collapse animation, transition end chains) into one
|
|
224
|
+
// `invalidateSize()` per animation frame. Also defuses the
|
|
225
|
+
// browser "ResizeObserver loop completed with undelivered
|
|
226
|
+
// notifications" warning if Leaflet's redraw chains back into a
|
|
227
|
+
// sub-element size change. We use schedule-once-per-frame
|
|
228
|
+
// (skip-if-pending) rather than cancel-and-reschedule — the
|
|
229
|
+
// pending callback already reads current DOM state at fire time,
|
|
230
|
+
// so the latest size is picked up without re-arming.
|
|
231
|
+
const raf = (globalThis as any).requestAnimationFrame as ((cb: () => void) => number) | undefined;
|
|
232
|
+
if (typeof raf !== 'function') {
|
|
233
|
+
this.invalidateSize();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (this._pendingResizeRaf != null) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this._pendingResizeRaf = raf(() => {
|
|
242
|
+
this._pendingResizeRaf = null;
|
|
243
|
+
this.invalidateSize();
|
|
152
244
|
});
|
|
153
245
|
});
|
|
246
|
+
this._resizeObserver.observe(el);
|
|
154
247
|
}
|
|
155
248
|
|
|
156
249
|
@Watch('bBox', { immediate: false, deep: true })
|
|
157
|
-
onBoundingBoxChanged
|
|
250
|
+
onBoundingBoxChanged() {
|
|
158
251
|
const bounds = this.getBoundingBoxLatLngBounds();
|
|
159
252
|
if (bounds) {
|
|
160
253
|
this.getLeaflet()?.fitBounds(bounds);
|
|
161
254
|
}
|
|
162
255
|
}
|
|
163
256
|
|
|
164
|
-
async waitForLeaflet
|
|
257
|
+
async waitForLeaflet(timeout = 15000, interval = 100) {
|
|
258
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
165
259
|
const startTime = Date.now();
|
|
166
260
|
while (!this.getLeaflet()) {
|
|
261
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
167
262
|
if (Date.now() - startTime > timeout) {
|
|
168
263
|
console.error('Leaflet initialization timed out.');
|
|
169
264
|
return null; // or throw an error, depending on your needs
|
|
@@ -174,27 +269,27 @@ export class OpenStreetMapComponent extends TsxComponent<OpenStreetMapArgs> impl
|
|
|
174
269
|
return this.getLeaflet();
|
|
175
270
|
}
|
|
176
271
|
|
|
177
|
-
initIcon
|
|
272
|
+
initIcon() {
|
|
178
273
|
if (this.leafletIcon == null) {
|
|
179
274
|
this.leafletIcon = OpenStreetMapHelper.getMarkerIcon();
|
|
180
275
|
}
|
|
181
276
|
}
|
|
182
277
|
|
|
183
278
|
@Watch('geoJSON')
|
|
184
|
-
onGeoJsonChanged
|
|
279
|
+
onGeoJsonChanged() {
|
|
185
280
|
if (this.geoJSONClustering) {
|
|
186
281
|
this.waitForLeaflet().then(() => {
|
|
187
|
-
this.initCluster()
|
|
188
|
-
})
|
|
282
|
+
this.initCluster();
|
|
283
|
+
});
|
|
189
284
|
}
|
|
190
285
|
}
|
|
191
286
|
|
|
192
287
|
@Watch('mapUrl')
|
|
193
|
-
onMapUrlChanged
|
|
288
|
+
onMapUrlChanged() {
|
|
194
289
|
this.invalidateSize();
|
|
195
290
|
}
|
|
196
291
|
|
|
197
|
-
initGestureHandling
|
|
292
|
+
initGestureHandling() {
|
|
198
293
|
if (this.gestureHandling == true) {
|
|
199
294
|
const leaflet = this.getLeaflet();
|
|
200
295
|
if ((leaflet as any)._gesturesBinder != true) {
|
|
@@ -204,10 +299,10 @@ export class OpenStreetMapComponent extends TsxComponent<OpenStreetMapArgs> impl
|
|
|
204
299
|
}
|
|
205
300
|
}
|
|
206
301
|
|
|
207
|
-
initCluster
|
|
302
|
+
initCluster() {
|
|
208
303
|
this.markerClusters?.clearLayers();
|
|
209
304
|
this.markerClusters = new LCluster.MarkerClusterGroup(this.clusterOptions || {
|
|
210
|
-
iconCreateFunction
|
|
305
|
+
iconCreateFunction(cluster) {
|
|
211
306
|
const html = `<div class="cluster-marker ">${cluster.getChildCount()}<div class="pulse-animation"></div></div>`;
|
|
212
307
|
return L.divIcon({ html, className: 'cluster-icon' });
|
|
213
308
|
},
|
|
@@ -225,7 +320,7 @@ export class OpenStreetMapComponent extends TsxComponent<OpenStreetMapArgs> impl
|
|
|
225
320
|
}
|
|
226
321
|
}
|
|
227
322
|
|
|
228
|
-
invalidateSize
|
|
323
|
+
invalidateSize() {
|
|
229
324
|
const leaflet = this.getLeaflet();
|
|
230
325
|
if (leaflet != null) {
|
|
231
326
|
this.$nextTick(() => {
|
|
@@ -234,11 +329,11 @@ export class OpenStreetMapComponent extends TsxComponent<OpenStreetMapArgs> impl
|
|
|
234
329
|
}
|
|
235
330
|
}
|
|
236
331
|
|
|
237
|
-
getNumeric
|
|
332
|
+
getNumeric(val: string): number {
|
|
238
333
|
return Number((val || '').toString().split(',').join(''));
|
|
239
334
|
}
|
|
240
335
|
|
|
241
|
-
getZoomValue
|
|
336
|
+
getZoomValue(): number {
|
|
242
337
|
if (this.defaultZoom != null) {
|
|
243
338
|
return this.defaultZoom;
|
|
244
339
|
} else if (this.bBox != null) {
|
|
@@ -248,60 +343,71 @@ export class OpenStreetMapComponent extends TsxComponent<OpenStreetMapArgs> impl
|
|
|
248
343
|
}
|
|
249
344
|
}
|
|
250
345
|
|
|
251
|
-
getCenter
|
|
346
|
+
getCenter() {
|
|
252
347
|
if (this.bBox != null) {
|
|
253
348
|
const { x1, x2, y1, y2 } = this.bBox;
|
|
254
349
|
const lat = (y1 + y2) / 2;
|
|
255
350
|
const lng = (x1 + x2) / 2;
|
|
256
|
-
return [
|
|
351
|
+
return [
|
|
352
|
+
lat,
|
|
353
|
+
lng,
|
|
354
|
+
];
|
|
257
355
|
}
|
|
258
356
|
|
|
259
357
|
const locArr = this.getLocArr();
|
|
260
358
|
return locArr;
|
|
261
359
|
}
|
|
262
360
|
|
|
263
|
-
getLocPoint
|
|
361
|
+
getLocPoint() {
|
|
264
362
|
return {
|
|
265
363
|
lat: this.getNumeric(this.latitude),
|
|
266
364
|
lng: this.getNumeric(this.longitude),
|
|
267
365
|
};
|
|
268
366
|
}
|
|
269
367
|
|
|
270
|
-
getLocArr
|
|
368
|
+
getLocArr() {
|
|
271
369
|
return [
|
|
272
370
|
this.getNumeric(this.latitude),
|
|
273
371
|
this.getNumeric(this.longitude),
|
|
274
372
|
];
|
|
275
373
|
}
|
|
276
374
|
|
|
277
|
-
getMap
|
|
375
|
+
getMap() {
|
|
278
376
|
return this.$refs.mainMap;
|
|
279
377
|
}
|
|
280
378
|
|
|
281
|
-
getTileLayerOptions
|
|
379
|
+
getTileLayerOptions() {
|
|
282
380
|
const options = { ...(this.options || {}) } as any;
|
|
283
381
|
if (typeof URL !== 'undefined' && typeof URL.canParse === 'function' && URL.canParse(this.mapUrl)) {
|
|
284
382
|
const urlHostname = new URL(this.mapUrl).hostname;
|
|
285
|
-
const osmHosts = [
|
|
383
|
+
const osmHosts = [
|
|
384
|
+
'tile.openstreetmap.org',
|
|
385
|
+
'tile.osm.org',
|
|
386
|
+
];
|
|
286
387
|
if (osmHosts.some(host => urlHostname.endsWith(host)) && options.referrerPolicy == null) {
|
|
287
388
|
options.referrerPolicy = 'strict-origin-when-cross-origin';
|
|
288
389
|
}
|
|
289
390
|
}
|
|
290
|
-
|
|
391
|
+
|
|
291
392
|
return options;
|
|
292
393
|
}
|
|
293
394
|
|
|
294
|
-
getLeaflet
|
|
395
|
+
getLeaflet() {
|
|
295
396
|
return (this.getMap() as any)?.leafletObject;
|
|
296
397
|
}
|
|
297
398
|
|
|
298
|
-
getBoundingBoxCorners
|
|
399
|
+
getBoundingBoxCorners() {
|
|
299
400
|
if (!this.bBox) {
|
|
300
401
|
return [];
|
|
301
402
|
}
|
|
302
403
|
|
|
303
404
|
const { x1, x2, y1, y2 } = this.bBox;
|
|
304
|
-
if ([
|
|
405
|
+
if ([
|
|
406
|
+
x1,
|
|
407
|
+
x2,
|
|
408
|
+
y1,
|
|
409
|
+
y2,
|
|
410
|
+
].some(p => p == null)) {
|
|
305
411
|
return [];
|
|
306
412
|
}
|
|
307
413
|
|
|
@@ -311,15 +417,30 @@ export class OpenStreetMapComponent extends TsxComponent<OpenStreetMapArgs> impl
|
|
|
311
417
|
const maxLng = Math.max(x1, x2);
|
|
312
418
|
|
|
313
419
|
return [
|
|
314
|
-
[
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
[
|
|
420
|
+
[
|
|
421
|
+
maxLat,
|
|
422
|
+
minLng,
|
|
423
|
+
],
|
|
424
|
+
[
|
|
425
|
+
maxLat,
|
|
426
|
+
maxLng,
|
|
427
|
+
],
|
|
428
|
+
[
|
|
429
|
+
minLat,
|
|
430
|
+
maxLng,
|
|
431
|
+
],
|
|
432
|
+
[
|
|
433
|
+
minLat,
|
|
434
|
+
minLng,
|
|
435
|
+
],
|
|
436
|
+
[
|
|
437
|
+
maxLat,
|
|
438
|
+
minLng,
|
|
439
|
+
],
|
|
319
440
|
];
|
|
320
441
|
}
|
|
321
442
|
|
|
322
|
-
getBoundingBoxLatLngBounds
|
|
443
|
+
getBoundingBoxLatLngBounds(): L.LatLngBounds {
|
|
323
444
|
const corners = this.getBoundingBoxCorners();
|
|
324
445
|
if (corners.length < 2) {
|
|
325
446
|
return null;
|
|
@@ -328,7 +449,7 @@ export class OpenStreetMapComponent extends TsxComponent<OpenStreetMapArgs> impl
|
|
|
328
449
|
return L.latLngBounds(corners);
|
|
329
450
|
}
|
|
330
451
|
|
|
331
|
-
render
|
|
452
|
+
render(h) {
|
|
332
453
|
if (this.is16by9 == false) {
|
|
333
454
|
return this.renderLeaflet(h, { height: '100%' });
|
|
334
455
|
}
|
|
@@ -346,17 +467,17 @@ export class OpenStreetMapComponent extends TsxComponent<OpenStreetMapArgs> impl
|
|
|
346
467
|
);
|
|
347
468
|
}
|
|
348
469
|
|
|
349
|
-
private renderLeaflet
|
|
470
|
+
private renderLeaflet(h, style: object) {
|
|
350
471
|
const locArr = this.getLocArr();
|
|
351
472
|
|
|
352
473
|
return (
|
|
353
474
|
<l-map style={style} zoom={this.getZoomValue()} options={this.mapOptions} maxZoom={this.maxZoom} center={this.getCenter()} ref="mainMap">
|
|
354
475
|
<l-tile-layer url={this.mapUrl} options={this.getTileLayerOptions()} noWrap attribution={'© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors'}></l-tile-layer>
|
|
355
476
|
{!this.isCenterHidden
|
|
356
|
-
|
|
477
|
+
&& <l-marker lat-lng={locArr} icon={this.leafletIcon}></l-marker>}
|
|
357
478
|
|
|
358
479
|
{!isNullOrEmpty(this.geoJSON)
|
|
359
|
-
|
|
480
|
+
&& <l-geo-json ref="geoJson" options={this.geoJSONConfig != null && this.geoJSONConfig} geojson={this.geoJSONClustering ? [] : this.geoJSON}></l-geo-json>}
|
|
360
481
|
|
|
361
482
|
{this.bBox && (
|
|
362
483
|
<l-polygon
|