@maplat/ui 0.11.10 → 0.11.11

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/src/ui_init.ts CHANGED
@@ -1,1425 +1,1438 @@
1
- import { MaplatApp as Core, createElement, MaplatApp } from "@maplat/core";
2
-
3
- import pointer from "./pointer_images";
4
- import { Swiper } from "./swiper_ex";
5
- import { Navigation, Pagination } from "swiper";
6
- import "swiper/swiper-bundle.css";
7
- import {
8
- SliderNew,
9
- Copyright,
10
- CompassRotate,
11
- Zoom,
12
- SetGPS,
13
- GoHome,
14
- Maplat,
15
- Share,
16
- Border,
17
- HideMarker,
18
- MarkerList
19
- } from "./maplat_control";
20
- import ContextMenu from "./contextmenu";
21
- import Weiwudi from "@c4h/weiwudi";
22
- import absoluteUrl from "./absolute_url";
23
- import * as QRCode from "qrcode";
24
- import { ellips, encBytes, isBasemap, prepareModal } from "./ui_utils";
25
-
26
- import { poiWebControl } from "./ui_marker";
27
-
28
- import type { MaplatUi } from "./index";
29
- import type { MaplatAppOption } from "./types";
30
-
31
- Swiper.use([Navigation, Pagination]);
32
-
33
- export const META_KEYS = [
34
- "title",
35
- "officialTitle",
36
- "author",
37
- "epoch",
38
- "createdAt",
39
- "era",
40
- "contributor",
41
- "mapper",
42
- "license",
43
- "dataLicense",
44
- "attr",
45
- "dataAttr",
46
- "reference",
47
- "description"
48
- ];
49
-
50
- export async function uiInit(ui: MaplatUi, appOption: MaplatAppOption) {
51
- appOption.translateUI = true;
52
- ui.core = new Core(appOption);
53
- if (appOption.icon) {
54
- (pointer as Record<string, string>)["defaultpin.png"] = appOption.icon;
55
- }
56
-
57
- if (appOption.restore) {
58
- ui.setShowBorder(appOption.restore.showBorder || false);
59
- if (appOption.restore.hideMarker) {
60
- ui.core!.waitReady.then(() => {
61
- ui.setHideMarker(appOption.restore!.hideMarker);
62
- });
63
- }
64
- if (appOption.restore.openedMarker) {
65
- console.log(appOption.restore.openedMarker);
66
- ui.core!.waitReady.then(() => {
67
- console.log(`Timeout ${appOption.restore!.openedMarker} `);
68
- ui.handleMarkerActionById(appOption.restore!.openedMarker!);
69
- });
70
- }
71
- } else if (appOption.restoreSession) {
72
- const lastEpoch = parseInt(
73
- String(localStorage.getItem("epoch") || "0"),
74
- 10
75
- );
76
- const currentTime = Math.floor(new Date().getTime() / 1000);
77
- if (lastEpoch && currentTime - lastEpoch < 3600) {
78
- ui.setShowBorder(
79
- !!parseInt(String(localStorage.getItem("showBorder") || "0"), 10)
80
- );
81
- }
82
- if (ui.core!.initialRestore.hideMarker) {
83
- ui.core!.waitReady.then(() => {
84
- ui.setHideMarker(true);
85
- });
86
- }
87
- } else {
88
- ui.setShowBorder(false);
89
- }
90
-
91
- ui.enablePoiHtmlNoScroll = appOption.enablePoiHtmlNoScroll || false;
92
- if (appOption.enableShare) {
93
- ui.core!.mapDivDocument!.classList.add("enable_share");
94
- ui.enableShare = true;
95
- }
96
- if (appOption.enableHideMarker) {
97
- ui.core!.mapDivDocument!.classList.add("enable_hide_marker");
98
- ui.enableHideMarker = true;
99
- }
100
- if (appOption.enableBorder) {
101
- ui.core!.mapDivDocument!.classList.add("enable_border");
102
- ui.enableBorder = true;
103
- }
104
- if (appOption.enableMarkerList) {
105
- ui.core!.mapDivDocument!.classList.add("enable_marker_list");
106
- ui.enableMarkerList = true;
107
- }
108
- if (appOption.disableNoimage) {
109
- ui.disableNoimage = true;
110
- }
111
- if (appOption.stateUrl) {
112
- ui.core!.mapDivDocument!.classList.add("state_url");
113
- }
114
- if (appOption.alwaysGpsOn) {
115
- ui.alwaysGpsOn = true;
116
- }
117
- if (ui.core!.enableCache) {
118
- ui.core!.mapDivDocument!.classList.add("enable_cache");
119
- }
120
- if ("ontouchstart" in window) {
121
- ui.core!.mapDivDocument!.classList.add("ol-touch");
122
- ui.isTouch = true;
123
- }
124
- if (appOption.mobileIF) {
125
- appOption.debug = true;
126
- }
127
- if (appOption.appEnvelope) {
128
- ui.appEnvelope = true;
129
- }
130
-
131
- initDom(ui, appOption);
132
- initModalHandlers(ui, appOption);
133
- initMapEventListeners(ui);
134
- initGpsHandlers(ui, appOption);
135
- }
136
-
137
- function initGpsHandlers(ui: MaplatUi, appOption: MaplatAppOption) {
138
- const enableOutOfMap = !appOption.presentationMode;
139
- const mapDiv = ui.core!.mapDivDocument!;
140
- const modalBase = mapDiv.querySelector(".modalBase")!;
141
- const modalTitle = mapDiv.querySelector(".modal_title") as HTMLElement;
142
- const modalGpsDContent = mapDiv.querySelector(
143
- ".modal_gpsD_content"
144
- ) as HTMLElement;
145
-
146
- ui.core!.addEventListener("outOfMap", (_evt: unknown) => {
147
- console.log("Event: outOfMap");
148
- if (enableOutOfMap) {
149
- modalTitle.innerText = ui.core!.t("app.out_of_map") || "";
150
- modalGpsDContent.innerText = ui.core!.t("app.out_of_map_area") || "";
151
- ui.modalSetting("gpsD");
152
- prepareModal(modalBase, { root: mapDiv }).show();
153
- }
154
- });
155
-
156
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
157
- ui.core!.addEventListener("gps_error", (evt: any) => {
158
- console.log("GPS Error:", evt);
159
- const errorMap: Record<string, string> = {
160
- user_gps_deny: "app.user_gps_deny",
161
- gps_miss: "app.gps_miss",
162
- gps_timeout: "app.gps_timeout"
163
- };
164
-
165
- if (!ui.core) return;
166
- modalTitle.innerText = ui.core.t("app.gps_error") || "";
167
- modalGpsDContent.innerText =
168
- ui.core.t(errorMap[evt.detail] || "app.gps_error") || "";
169
- ui.modalSetting("gpsD");
170
- prepareModal(modalBase, {
171
- root: mapDiv
172
- }).show();
173
- });
174
-
175
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
176
- ui.core!.addEventListener("gps_result", (evt: any) => {
177
- console.log("GPS Result:", evt);
178
- if (evt.detail && evt.detail.error) {
179
- const error = evt.detail.error;
180
- if (error === "gps_off") {
181
- ui.lastGPSError = undefined;
182
- return;
183
- }
184
-
185
- ui.lastGPSError = error;
186
- if (ui.alwaysGpsOn && error === "gps_out") return;
187
-
188
- if (!ui.core) return;
189
-
190
- const modal = prepareModal(modalBase, {
191
- root: mapDiv
192
- });
193
-
194
- if (error === "gps_out") {
195
- modalTitle.innerText = ui.core.t("app.out_of_map") || "";
196
- modalGpsDContent.innerText = ui.core.t("app.out_of_map_area") || "";
197
- } else {
198
- const errorMap: Record<string, string> = {
199
- user_gps_deny: "app.user_gps_deny",
200
- gps_miss: "app.gps_miss",
201
- gps_timeout: "app.gps_timeout"
202
- };
203
-
204
- modalTitle.innerText = ui.core.t("app.gps_error") || "";
205
- modalGpsDContent.innerText =
206
- ui.core.t(errorMap[error] || "app.gps_error") || "";
207
- }
208
-
209
- ui.modalSetting("gpsD");
210
- modal.show();
211
- } else {
212
- ui.lastGPSError = undefined;
213
- }
214
- });
215
- }
216
-
217
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
218
- function initSwipers(ui: MaplatUi, sources: any[]) {
219
- const colors = [
220
- "maroon",
221
- "deeppink",
222
- "indigo",
223
- "olive",
224
- "royalblue",
225
- "red",
226
- "hotpink",
227
- "green",
228
- "yellow",
229
- "navy",
230
- "saddlebrown",
231
- "fuchsia",
232
- "darkslategray",
233
- "yellowgreen",
234
- "blue",
235
- "mediumvioletred",
236
- "purple",
237
- "lime",
238
- "darkorange",
239
- "teal",
240
- "crimson",
241
- "darkviolet",
242
- "darkolivegreen",
243
- "steelblue",
244
- "aqua"
245
- ];
246
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
247
- const appBbox: any[] = [];
248
- let cIndex = 0;
249
-
250
- for (let i = 0; i < sources.length; i++) {
251
- const source = sources[i];
252
- if (source.envelope) {
253
- if (ui.appEnvelope) {
254
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
255
- source.envelope.geometry.coordinates[0].map((xy: any) => {
256
- if (appBbox.length === 0) {
257
- appBbox[0] = appBbox[2] = xy[0];
258
- appBbox[1] = appBbox[3] = xy[1];
259
- } else {
260
- if (xy[0] < appBbox[0]) appBbox[0] = xy[0];
261
- if (xy[0] > appBbox[2]) appBbox[2] = xy[0];
262
-
263
- if (xy[1] < appBbox[1]) appBbox[1] = xy[1];
264
- if (xy[1] > appBbox[3]) appBbox[3] = xy[1];
265
- }
266
- });
267
- }
268
- source.envelopeColor = colors[cIndex];
269
- cIndex++;
270
- if (cIndex === colors.length) cIndex = 0;
271
-
272
- const xys = source.envelope.geometry.coordinates[0];
273
- source.envelopeAreaIndex = ui.areaIndex(xys);
274
- }
275
- }
276
- if (ui.appEnvelope) console.log(`This app's envelope is: ${appBbox}`);
277
-
278
- if (ui.splashPromise) {
279
- ui.splashPromise.then(() => {
280
- const modalElm = ui.core!.mapDivDocument!.querySelector(".modalBase")!;
281
- prepareModal(modalElm, { root: ui.core!.mapDivDocument! }).hide();
282
- });
283
- }
284
-
285
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
286
- const baseSources: any[] = [];
287
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
288
- const overlaySources: any[] = [];
289
- sources.forEach(source => {
290
- if (isBasemap(source)) {
291
- baseSources.push(source);
292
- } else {
293
- overlaySources.push(source);
294
- }
295
- });
296
-
297
- const baseSwiper = (ui.baseSwiper = new Swiper(".base-swiper", {
298
- slidesPerView: 2,
299
- spaceBetween: 15,
300
- breakpoints: {
301
- 480: {
302
- slidesPerView: 1.4,
303
- spaceBetween: 10
304
- }
305
- },
306
- centeredSlides: true,
307
- threshold: 2,
308
- preventClicks: true,
309
- preventClicksPropagation: true,
310
- observer: true,
311
- observeParents: true,
312
- loop: baseSources.length >= 2,
313
- navigation: {
314
- nextEl: ".base-next",
315
- prevEl: ".base-prev"
316
- }
317
- }));
318
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
319
- baseSwiper.on("click", (_e: any) => {
320
- if (!baseSwiper.clickedSlide) return;
321
- const slide = baseSwiper.clickedSlide;
322
- ui.core!.changeMap(slide.getAttribute("data")!);
323
- delete ui._selectCandidateSources;
324
- baseSwiper.setSlideIndexAsSelected(
325
- parseInt(slide.getAttribute("data-swiper-slide-index") || "0", 10)
326
- );
327
- });
328
- if (baseSources.length < 2) {
329
- ui.core!.mapDivDocument!.querySelector(".base-swiper")!.classList.add(
330
- "single-map"
331
- );
332
- }
333
-
334
- const overlaySwiper = (ui.overlaySwiper = new Swiper(".overlay-swiper", {
335
- slidesPerView: 2,
336
- spaceBetween: 15,
337
- breakpoints: {
338
- 480: {
339
- slidesPerView: 1.4,
340
- spaceBetween: 10
341
- }
342
- },
343
- centeredSlides: true,
344
- threshold: 2,
345
- preventClicks: true,
346
- preventClicksPropagation: true,
347
- observer: true,
348
- observeParents: true,
349
- loop: overlaySources.length >= 2,
350
- navigation: {
351
- nextEl: ".overlay-next",
352
- prevEl: ".overlay-prev"
353
- }
354
- }));
355
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
356
- overlaySwiper.on("click", (_e: any) => {
357
- if (!overlaySwiper.clickedSlide) return;
358
- const slide = overlaySwiper.clickedSlide;
359
- ui.core!.changeMap(slide.getAttribute("data")!);
360
- delete ui._selectCandidateSources;
361
- overlaySwiper.setSlideIndexAsSelected(
362
- parseInt(slide.getAttribute("data-swiper-slide-index") || "0", 10)
363
- );
364
- });
365
- if (overlaySources.length < 2) {
366
- ui.core!.mapDivDocument!.querySelector(".overlay-swiper")!.classList.add(
367
- "single-map"
368
- );
369
- }
370
-
371
- baseSources.forEach(source => {
372
- const thumbKey = source.thumbnail ? source.thumbnail.split("/").pop() : "";
373
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
374
- const thumbUrl = (pointer as any)[thumbKey] || source.thumbnail;
375
- baseSwiper.appendSlide(
376
- `<div class="swiper-slide" data="${source.mapID}">` +
377
- `<img crossorigin="anonymous" src="${
378
- thumbUrl
379
- }"><div> ${ui.core!.translate(source.label)}</div> </div> `
380
- );
381
- });
382
- overlaySources.forEach(source => {
383
- const colorCss = source.envelope ? ` ${source.envelopeColor}` : "";
384
- const thumbKey = source.thumbnail ? source.thumbnail.split("/").pop() : "";
385
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
386
- const thumbUrl = (pointer as any)[thumbKey] || source.thumbnail;
387
- overlaySwiper.appendSlide(
388
- `<div class="swiper-slide${colorCss}" data="${source.mapID}">` +
389
- `<img crossorigin="anonymous" src="${
390
- thumbUrl
391
- }"><div> ${ui.core!.translate(source.label)}</div> </div> `
392
- );
393
- });
394
-
395
- overlaySwiper.on("slideChange", () => {
396
- ui.updateEnvelope();
397
- });
398
-
399
- baseSwiper.slideToLoop(0);
400
- overlaySwiper.slideToLoop(0);
401
- ellips(ui.core!.mapDivDocument!);
402
- }
403
-
404
- function initMapEventListeners(ui: MaplatUi) {
405
- ui.core!.addEventListener("mapChanged", (evt: unknown) => {
406
- const map = (evt as CustomEvent).detail;
407
-
408
- ui.baseSwiper.setSlideMapID(map.mapID);
409
- ui.overlaySwiper.setSlideMapID(map.mapID);
410
-
411
- const title = map.officialTitle || map.title || map.label;
412
- (
413
- ui.core!.mapDivDocument!.querySelector(".map-title span") as HTMLElement
414
- ).innerText = ui.core!.translate(title) || "";
415
-
416
- if (ui.checkOverlayID(map.mapID)) {
417
- ui.sliderNew.setEnable(true);
418
- } else {
419
- ui.sliderNew.setEnable(false);
420
- }
421
- const transparency = ui.sliderNew.get("slidervalue") * 100;
422
- ui.core!.mapObject.setTransparency(transparency);
423
-
424
- ui.updateEnvelope();
425
- ui.updateUrl();
426
- });
427
-
428
- ui.core!.addEventListener("poi_number", (evt: unknown) => {
429
- const number = (evt as CustomEvent).detail;
430
- if (number) {
431
- ui.core!.mapDivDocument!.classList.remove("no_poi");
432
- } else {
433
- ui.core!.mapDivDocument!.classList.add("no_poi");
434
- }
435
- });
436
-
437
- ui.core!.addEventListener("sourceLoaded", (evt: unknown) => {
438
- const sources = (evt as CustomEvent).detail;
439
- initSwipers(ui, sources);
440
- });
441
-
442
- ui.core!.waitReady.then(() => {
443
- // Capture pointerdown at viewport level to ensure we get pixel before any stopPropagation
444
-
445
- ui.core!.mapObject.getViewport().addEventListener(
446
- "pointerdown",
447
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
448
- (evt: any) => {
449
- ui.lastClickPixel = ui.core!.mapObject.getEventPixel(evt);
450
- ui.lastClickCoordinate = ui.core!.mapObject.getCoordinateFromPixel(
451
- ui.lastClickPixel
452
- );
453
- },
454
- true
455
- );
456
- });
457
-
458
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
459
- ui.core!.addEventListener("clickMarkers", (evt: any) => {
460
- const data = evt.detail;
461
- if (data.length === 1) {
462
- ui.handleMarkerAction(data[0]);
463
- } else {
464
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
465
- const list: any[] = [];
466
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
467
- data.forEach((datum: any) => {
468
- list.push({
469
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
470
- icon: datum.icon || (pointer as any)["defaultpin.png"],
471
- text: ui.core!.translate(datum.name),
472
- callback: () => {
473
- ui.handleMarkerAction(datum);
474
- }
475
- });
476
- });
477
- ui.showContextMenu(list);
478
- }
479
- });
480
-
481
- ui.core!.waitReady.then(() => {
482
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
483
- ui.core!.mapObject.on("moveend", (_evt: any) => {
484
- ui.updateUrl();
485
- });
486
- });
487
- }
488
-
489
- function initModalHandlers(ui: MaplatUi, appOption: MaplatAppOption) {
490
- let cachedMarkerListMapID: string | undefined;
491
- const mapDiv = ui.core!.mapDivDocument!;
492
- const modalBase = mapDiv.querySelector(".modalBase") as HTMLElement;
493
-
494
- const restoreTransparency =
495
- ui.core!.initialRestore.transparency ||
496
- (appOption.restore ? appOption.restore.transparency : undefined);
497
- const enableSplash = !ui.core!.initialRestore.mapID;
498
-
499
- // Delegated event listener for share buttons
500
- mapDiv.addEventListener("click", (evt: Event) => {
501
- const target = evt.target as HTMLElement;
502
- const btn = target.closest(".share") || target.closest(".share_button");
503
- if (!btn) return;
504
-
505
- console.log("Share button clicked:", btn);
506
- const cmd = btn.getAttribute("data");
507
- if (!cmd) return;
508
- const cmds = cmd.split("_");
509
- const uri = ui.getShareUrl(cmds[1] || "app");
510
-
511
- console.log("Share URI:", uri);
512
-
513
- if (cmds[0] === "cp") {
514
- const bodyElm = document.querySelector("body")!;
515
- const message = ui.core!.t ? ui.core!.t("app.copy_toast") : "URL Copied";
516
- if (navigator.clipboard) {
517
- navigator.clipboard.writeText(uri).then(() => {
518
- ui.showToast(message, btn as HTMLElement);
519
- });
520
- } else {
521
- const copyFrom = document.createElement("textarea");
522
- copyFrom.textContent = uri;
523
- bodyElm.appendChild(copyFrom);
524
- copyFrom.select();
525
- document.execCommand("copy");
526
- bodyElm.removeChild(copyFrom);
527
- ui.showToast(message, btn as HTMLElement);
528
- }
529
- } else if (cmds[0] === "tw") {
530
- const text = document.title;
531
- const twuri = `https://twitter.com/intent/tweet?url=${encodeURIComponent(uri)}&text=${encodeURIComponent(text)}&hashtags=Maplat`;
532
- window.open(twuri, "_blank");
533
- } else if (cmds[0] === "fb") {
534
- const fburi = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(uri)}&display=popup&ref=plugin&src=like&kid_directed_site=0`;
535
- window.open(
536
- fburi,
537
- "_blank",
538
- "width=650,height=450,menubar=no,toolbar=no,scrollbars=yes"
539
- );
540
- } else if (cmds[0] === "qr") {
541
- const qrDiv =
542
- mapDiv.querySelector(".qr_view_poi") ||
543
- mapDiv.querySelector(".qr_view");
544
-
545
- if (qrDiv) {
546
- QRCode.toCanvas(
547
- uri,
548
- { width: 128, margin: 1 },
549
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
550
- (err: any, canvas: any) => {
551
- if (!err) {
552
- qrDiv.innerHTML = "";
553
- qrDiv.appendChild(canvas);
554
- }
555
- }
556
- );
557
- }
558
- }
559
- });
560
-
561
- const prevDefs = mapDiv.querySelectorAll(".prevent-default-ui");
562
- prevDefs.forEach(target => {
563
- target.addEventListener("touchstart", (evt: Event) => {
564
- evt.preventDefault();
565
- });
566
- });
567
-
568
- ui.core!.addEventListener("uiPrepare", (_evt: unknown) => {
569
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
570
- const imageExtractor = function (text: any) {
571
- const regexp = /\$\{([a-zA-Z0-9_\.\/\-]+)\}/g; // eslint-disable-line no-useless-escape
572
- let ret = text;
573
- let match;
574
- while ((match = regexp.exec(text)) != null) {
575
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
576
- ret = ret.replace(match[0], (pointer as any)[match[1]]);
577
- }
578
- return ret;
579
- };
580
-
581
- let i18nTargets = mapDiv.querySelectorAll("[data-i18n], [din]");
582
- i18nTargets.forEach(target => {
583
- const key =
584
- target.getAttribute("data-i18n") || target.getAttribute("din");
585
- (target as HTMLElement).innerText = imageExtractor(ui.core!.t(key));
586
- });
587
- i18nTargets = mapDiv.querySelectorAll("[data-i18n-html], [dinh]");
588
- i18nTargets.forEach(target => {
589
- const key =
590
- target.getAttribute("data-i18n-html") || target.getAttribute("dinh");
591
- target.innerHTML = imageExtractor(ui.core!.t(key));
592
- });
593
- // Explicitly fix app_loading_body with a more robust selector if needed, or re-run translation for it
594
- const appLoadingBody = mapDiv.querySelector(
595
- '[data-i18n="html.app_loading_body"], [din="html.app_loading_body"]'
596
- );
597
- if (appLoadingBody) {
598
- (appLoadingBody as HTMLElement).innerHTML = imageExtractor(
599
- ui.core!.t("html.app_loading_body")
600
- );
601
- }
602
-
603
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
604
- const options: any = {
605
- reverse: true,
606
- tipLabel: ui.core!.t("control.trans", { ns: "translation" })
607
- };
608
- if (restoreTransparency) {
609
- options.initialValue = restoreTransparency / 100;
610
- }
611
- ui.sliderNew = new SliderNew(options);
612
- ui.core!.appData!.controls = [
613
- new Copyright({
614
- tipLabel: ui.core!.t("control.info", { ns: "translation" })
615
- }),
616
- new CompassRotate({
617
- tipLabel: ui.core!.t("control.compass", { ns: "translation" })
618
- }),
619
- new Zoom({
620
- tipLabel: ui.core!.t("control.zoom", { ns: "translation" })
621
- }),
622
- new SetGPS({
623
- ui,
624
- tipLabel: ui.core!.t("control.gps", { ns: "translation" })
625
- }),
626
- new GoHome({
627
- tipLabel: ui.core!.t("control.home", { ns: "translation" })
628
- }),
629
- ui.sliderNew,
630
- new Maplat({
631
- tipLabel: ui.core!.t("control.help", { ns: "translation" })
632
- })
633
- ];
634
- if (ui.enableShare) {
635
- ui.core!.appData!.controls.push(
636
- new Share({
637
- tipLabel: ui.core!.t("control.share", { ns: "translation" })
638
- })
639
- );
640
- }
641
- if (ui.enableBorder) {
642
- ui.core!.appData!.controls.push(
643
- new Border({
644
- tipLabel: ui.core!.t("control.border", { ns: "translation" })
645
- })
646
- );
647
- }
648
- if (ui.enableHideMarker) {
649
- ui.core!.appData!.controls.push(
650
- new HideMarker({
651
- tipLabel: ui.core!.t("control.hide_marker", { ns: "translation" })
652
- })
653
- );
654
- }
655
- if (ui.enableMarkerList) {
656
- ui.core!.appData!.controls.push(
657
- new MarkerList({
658
- tipLabel: ui.core!.t("control.marker_list", { ns: "translation" })
659
- })
660
- );
661
- }
662
-
663
- // Contextmenu
664
- ui.contextMenu = new ContextMenu({
665
- eventType: "__dummy__",
666
- width: 170,
667
- defaultItems: false,
668
- items: []
669
- });
670
- ui.core!.appData!.controls.push(ui.contextMenu);
671
-
672
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
673
- ui.sliderNew.on("propertychange", (evt: any) => {
674
- if (evt.key === "slidervalue") {
675
- ui.core!.setTransparency(ui.sliderNew.get(evt.key) * 100);
676
- ui.updateUrl();
677
- }
678
- });
679
-
680
- if (enableSplash) {
681
- // Check Splash data
682
- let splash = false;
683
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
684
- if ((ui.core!.appData as any).splash) splash = true;
685
-
686
- // const modal = new bsn.Modal(modalElm, { root: mapDiv });
687
- const modal = prepareModal(modalBase, { root: mapDiv });
688
-
689
- (mapDiv.querySelector(".modal_load_title") as HTMLElement).innerText =
690
- ui.core!.translate(ui.core!.appData!.appName) || "";
691
- if (splash) {
692
- mapDiv
693
- .querySelector(".splash_img")!
694
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
695
- .setAttribute("src", `img/${(ui.core!.appData as any).splash}`);
696
- mapDiv.querySelector(".splash_div")!.classList.remove("hide");
697
- }
698
- ui.modalSetting("load");
699
- modal.show();
700
-
701
- const fadeTime = splash ? 1000 : 200;
702
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
703
- ui.splashPromise = new Promise((resolve: any) => {
704
- setTimeout(() => {
705
- resolve();
706
- }, fadeTime);
707
- });
708
- }
709
-
710
- document.querySelector("title")!.innerHTML =
711
- ui.core!.translate(ui.core!.appName) || "";
712
- });
713
-
714
- ui.core!.waitReady.then(() => {
715
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
716
- ui.core!.mapObject.on("click_control", (evt: any) => {
717
- const control = evt.control || (evt.frameState && evt.frameState.control);
718
- const modal = prepareModal(modalBase);
719
-
720
- if (control === "help") {
721
- ui.modalSetting("help");
722
- modal.show();
723
- } else if (control === "share") {
724
- ui.modalSetting("share");
725
-
726
- const modalBody = modalBase.querySelector(".modal-body") as HTMLElement;
727
-
728
- const baseUrl = ui.getShareUrl("app");
729
- const viewUrl = ui.getShareUrl("view");
730
-
731
- // Generate QR Codes
732
- const qrAppDiv = modalBody.querySelector(".qr_app") as HTMLElement;
733
- const qrViewDiv = modalBody.querySelector(".qr_view") as HTMLElement;
734
-
735
- if (qrAppDiv) {
736
- QRCode.toCanvas(
737
- baseUrl,
738
- { width: 128, margin: 1 },
739
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
740
- (err: any, canvas: any) => {
741
- if (!err) {
742
- qrAppDiv.innerHTML = "";
743
- qrAppDiv.appendChild(canvas);
744
- }
745
- }
746
- );
747
- }
748
- if (qrViewDiv) {
749
- QRCode.toCanvas(
750
- viewUrl,
751
- { width: 128, margin: 1 },
752
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
753
- (err: any, canvas: any) => {
754
- if (!err) {
755
- qrViewDiv.innerHTML = "";
756
- qrViewDiv.appendChild(canvas);
757
- }
758
- }
759
- );
760
- }
761
-
762
- modal.show();
763
- } else if (control === "markerList") {
764
- ui.modalSetting("marker_list");
765
- modal.show();
766
-
767
- const listRoot = modalBase.querySelector(
768
- ".modal_marker_list_content ul.list-group"
769
- ) as HTMLElement;
770
-
771
- // Reset all panel states when modal is closed
772
- const resetPanels = () => {
773
- // Close all Layer panels
774
- const allLayerPanels =
775
- listRoot.querySelectorAll(".list_poiitems_div");
776
- allLayerPanels.forEach(panel => {
777
- panel.classList.remove("open");
778
- });
779
-
780
- // Close and reset all POI Content panels
781
- const allPoiContentDivs = listRoot.querySelectorAll(
782
- ".list_poicontent_div"
783
- );
784
- allPoiContentDivs.forEach(contentDiv => {
785
- contentDiv.classList.remove("open");
786
- contentDiv.innerHTML = "";
787
- });
788
-
789
- // Unselect marker
790
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
791
- (ui.core as any).unselectMarker?.();
792
-
793
- // Remove this event listener after execution
794
- modalBase.removeEventListener("hidden.bs.modal", resetPanels);
795
- };
796
-
797
- // Remove old listener if exists and add new one
798
- modalBase.removeEventListener("hidden.bs.modal", resetPanels);
799
- modalBase.addEventListener("hidden.bs.modal", resetPanels);
800
-
801
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
802
- const currentMapID = (ui.core!.from as any).mapID;
803
- if (cachedMarkerListMapID === currentMapID) return;
804
- cachedMarkerListMapID = currentMapID;
805
-
806
- listRoot.innerHTML = "";
807
-
808
- const layers = ui.core!.listPoiLayers(false, true);
809
-
810
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
811
- layers.forEach((layer: any) => {
812
- // Create Layer Item
813
- const layerLi = createElement(`<li class="list-group-item layer">
814
- <div class="row layer_row">
815
- <div class="layer_label">
816
- <span class="dli-chevron"></span>
817
- <img src="${layer.icon || pointer["defaultpin.png"]}" class="markerlist"> ${ui.core!.translate(layer.name)}
818
- </div>
819
- <div class="layer_onoff">
820
- <input type="checkbox" class="markerlist" ${layer.hide ? "" : "checked"}>
821
- <label class="check"><div></div></label>
822
- </div>
823
- </div>
824
- </li>`)[0] as HTMLElement;
825
-
826
- const checkbox = layerLi.querySelector(
827
- "input[type=checkbox]"
828
- ) as HTMLInputElement;
829
- const label = layerLi.querySelector("label.check") as HTMLElement;
830
-
831
- label.addEventListener("click", e => {
832
- e.stopPropagation();
833
- if (!checkbox.disabled) {
834
- checkbox.checked = !checkbox.checked;
835
- checkbox.dispatchEvent(new Event("change"));
836
- }
837
- });
838
-
839
- checkbox.addEventListener("click", e => e.stopPropagation());
840
-
841
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
842
- checkbox.addEventListener("change", (e: any) => {
843
- if (e.target.checked) {
844
- ui.core!.showPoiLayer(layer.id);
845
- } else {
846
- ui.core!.hidePoiLayer(layer.id);
847
- }
848
- });
849
-
850
- const poiListUl = createElement(
851
- `<ul class="list_poiitems_div"></ul>`
852
- )[0] as HTMLElement;
853
-
854
- layerLi
855
- .querySelector(".layer_label")!
856
- .addEventListener("click", () => {
857
- const isCurrentlyOpen = poiListUl.classList.contains("open");
858
-
859
- // Close all other Layer panels at the same level
860
- const allLayerPanels =
861
- listRoot.querySelectorAll(".list_poiitems_div");
862
- allLayerPanels.forEach(panel => {
863
- if (panel !== poiListUl) {
864
- panel.classList.remove("open");
865
- // Reset all child POI Content panels
866
- const poiContentDivs = panel.parentElement!.querySelectorAll(
867
- ".list_poicontent_div"
868
- );
869
- poiContentDivs.forEach(contentDiv => {
870
- contentDiv.classList.remove("open");
871
- contentDiv.innerHTML = "";
872
- });
873
- }
874
- });
875
-
876
- // Toggle current panel
877
- if (isCurrentlyOpen) {
878
- poiListUl.classList.remove("open");
879
- // Reset all child POI Content panels in this Layer
880
- const poiContentDivs = poiListUl.querySelectorAll(
881
- ".list_poicontent_div"
882
- );
883
- poiContentDivs.forEach(contentDiv => {
884
- contentDiv.classList.remove("open");
885
- contentDiv.innerHTML = "";
886
- });
887
- (ui.core as MaplatApp).unselectMarker?.();
888
- } else {
889
- poiListUl.classList.add("open");
890
- }
891
- });
892
-
893
- listRoot.appendChild(layerLi);
894
- listRoot.appendChild(poiListUl);
895
-
896
- if (layer.pois) {
897
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
898
- layer.pois.forEach((poi: any) => {
899
- const poiLi = createElement(`<li class="list-group-item poi">
900
- <div class="row poi_row">
901
- <div class="poi_label">
902
- <span class="dli-chevron"></span>
903
- <img src="${poi.icon || layer.icon || pointer["defaultpin.png"]}" class="markerlist"> ${ui.core!.translate(poi.name)}
904
- </div>
905
- </div>
906
- </li>`)[0] as HTMLElement;
907
-
908
- const poiContentDiv = createElement(
909
- `<div class="list_poicontent_div"></div>`
910
- )[0] as HTMLElement;
911
-
912
- // let poiImgHide: any;
913
-
914
- poiLi.addEventListener("click", () => {
915
- const isCurrentlyOpen =
916
- poiContentDiv.classList.contains("open");
917
-
918
- // Close all other POI panels at the same level (within the same Layer)
919
- const allPoiContentDivs = poiListUl.querySelectorAll(
920
- ".list_poicontent_div"
921
- );
922
- allPoiContentDivs.forEach(contentDiv => {
923
- if (
924
- contentDiv !== poiContentDiv &&
925
- contentDiv.classList.contains("open")
926
- ) {
927
- contentDiv.classList.remove("open");
928
- contentDiv.innerHTML = "";
929
- }
930
- });
931
-
932
- // Toggle current panel
933
- if (isCurrentlyOpen) {
934
- poiContentDiv.classList.remove("open");
935
- poiContentDiv.innerHTML = "";
936
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
937
- (ui.core as any).unselectMarker?.();
938
- } else {
939
- poiContentDiv.classList.add("open");
940
- poiWebControl(ui, poiContentDiv, poi, false);
941
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
942
- (ui.core as any).selectMarker?.(poi.namespaceID);
943
- }
944
- });
945
-
946
- poiListUl.appendChild(poiLi);
947
- poiListUl.appendChild(poiContentDiv);
948
- });
949
- }
950
- });
951
- } else if (control === "copyright") {
952
- ui.modalSetting("map");
953
- const mapData = ui.core!.from!;
954
- const titleEl = mapDiv.querySelector(".modal_map .modal_title");
955
- if (titleEl) {
956
- const titleVal = mapData.get ? mapData.get("title") : mapData.title;
957
- (titleEl as HTMLElement).innerText =
958
- ui.core!.translate(titleVal) || "";
959
- }
960
-
961
- META_KEYS.forEach(key => {
962
- if (key === "title" || key === "officialTitle") return;
963
- const val = mapData.get
964
- ? mapData.get(key)
965
- : // eslint-disable-next-line @typescript-eslint/no-explicit-any
966
- (mapData as any)[key];
967
- const container = mapDiv.querySelector(`.modal_map .${key}_div`);
968
- if (container) {
969
- if (val) {
970
- (container as HTMLElement).style.display = "block";
971
- const contentEl = container.querySelector(`.${key}_dd`);
972
- if (contentEl) {
973
- if (key === "license" || key === "dataLicense") {
974
- const fileName = (val as string)
975
- .toLowerCase()
976
- .replace(/ /g, "_");
977
-
978
- const iconUrl =
979
- pointer[`${fileName}.png`] ||
980
- `assets/parts/${fileName}.png`;
981
- (contentEl as HTMLElement).innerHTML =
982
- `<img src="${iconUrl}" class="license" />`;
983
- } else {
984
- (contentEl as HTMLElement).innerHTML =
985
- ui.core!.translate(val) || "";
986
- }
987
- }
988
- } else {
989
- (container as HTMLElement).style.display = "none";
990
- }
991
- }
992
- });
993
-
994
- const cacheDiv = mapDiv.querySelector(
995
- ".modal_cache_content"
996
- ) as HTMLElement;
997
- const cacheSize = cacheDiv.querySelector(".cache_size") as HTMLElement;
998
- let cacheFetch = cacheDiv.querySelector(
999
- ".cache_fetch"
1000
- ) as HTMLButtonElement;
1001
- let cacheDelete = cacheDiv.querySelector(
1002
- ".cache_delete"
1003
- ) as HTMLButtonElement;
1004
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1005
- const weiwudi = (mapData as any).weiwudi;
1006
- if (
1007
- ui.core!.enableCache &&
1008
- weiwudi &&
1009
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1010
- !(mapData as any).vector
1011
- ) {
1012
- cacheDiv.style.display = "block";
1013
- cacheFetch.style.display = "none";
1014
- cacheDelete.style.display = "none";
1015
- const totalTile = weiwudi.totalTile;
1016
- let isFetching = false;
1017
-
1018
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1019
- let currentStats: any = undefined;
1020
-
1021
- const updateButtons = () => {
1022
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1023
- const coreAny = ui.core as any;
1024
- const t = coreAny.t
1025
- ? coreAny.t.bind(ui.core)
1026
- : ui.core!.translate.bind(ui.core);
1027
-
1028
- if (totalTile) {
1029
- cacheFetch.style.display = "inline-block";
1030
- if (isFetching) {
1031
- cacheFetch.innerHTML =
1032
- t("html.cache_cancel") || "Cancel download";
1033
- cacheFetch.classList.remove("btn-default");
1034
- cacheFetch.classList.add("btn-danger");
1035
- if (!cacheFetch.classList.contains("btn-default"))
1036
- cacheFetch.classList.add("btn-default");
1037
- cacheFetch.disabled = false;
1038
- } else {
1039
- cacheFetch.innerHTML = t("html.cache_fetch") || "Bulk download";
1040
- if (!cacheFetch.classList.contains("btn-default"))
1041
- cacheFetch.classList.add("btn-default");
1042
- cacheFetch.classList.remove("btn-danger");
1043
-
1044
- // Disable if 100%
1045
- if (currentStats && currentStats.count === currentStats.total) {
1046
- cacheFetch.disabled = true;
1047
- // User requested disabled, not hidden
1048
- cacheFetch.style.display = "inline-block";
1049
- } else {
1050
- cacheFetch.disabled = false;
1051
- cacheFetch.style.display = "inline-block";
1052
- }
1053
- }
1054
- } else {
1055
- cacheFetch.style.display = "none";
1056
- }
1057
-
1058
- if (currentStats && currentStats.size > 0) {
1059
- cacheDelete.style.display = "inline-block";
1060
- // Disable if fetching
1061
- cacheDelete.disabled = isFetching;
1062
- } else {
1063
- // User requested disabled, not hidden
1064
- cacheDelete.style.display = "inline-block";
1065
- cacheDelete.disabled = true;
1066
- }
1067
- if (isFetching) cacheDelete.disabled = true; // Double ensure
1068
- };
1069
-
1070
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1071
- const showStats = async (stats: any | undefined = undefined) => {
1072
- if (!stats) stats = await weiwudi.stats();
1073
- currentStats = stats;
1074
-
1075
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1076
- const coreAny = ui.core as any;
1077
- const t = coreAny.t
1078
- ? coreAny.t.bind(ui.core)
1079
- : ui.core!.translate.bind(ui.core);
1080
-
1081
- const sizeStr = isFetching
1082
- ? t("html.cache_processing") || "Calculating..."
1083
- : encBytes(stats.size || 0);
1084
-
1085
- if (totalTile) {
1086
- const count = stats.count || 0;
1087
- const percent = Math.floor((1000 * count) / totalTile);
1088
- cacheSize.innerText = `${sizeStr} (${
1089
- count
1090
- } / ${totalTile} tiles [${percent / 10}%])`;
1091
- } else {
1092
- cacheSize.innerText = `${sizeStr} (${stats.count || 0} tiles)`;
1093
- }
1094
- updateButtons();
1095
- };
1096
- showStats();
1097
-
1098
- let updateFrame = 0;
1099
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1100
- let latestStats: any = null;
1101
-
1102
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1103
- const fetchHandler = (evt: any) => {
1104
- if (evt.type === "proceed") {
1105
- isFetching = true;
1106
- latestStats = {
1107
- size: evt.detail.size || currentStats?.size || 0,
1108
- count: evt.detail.processed || evt.detail.count || 0,
1109
- total: evt.detail.total || totalTile || 0
1110
- };
1111
-
1112
- if (!updateFrame) {
1113
- updateFrame = requestAnimationFrame(() => {
1114
- updateFrame = 0;
1115
- try {
1116
- if (latestStats) showStats(latestStats);
1117
- } catch (e) {
1118
- console.error("Error in showStats:", e);
1119
- }
1120
- });
1121
- }
1122
- } else if (
1123
- evt.type === "finish" ||
1124
- evt.type === "stop" ||
1125
- evt.type === "canceled"
1126
- ) {
1127
- if (updateFrame) {
1128
- cancelAnimationFrame(updateFrame);
1129
- updateFrame = 0;
1130
- }
1131
- isFetching = false;
1132
- latestStats = null;
1133
- showStats();
1134
- }
1135
- };
1136
- // Weiwudi might dispatch 'proceed', 'finish', 'stop', 'canceled'
1137
- weiwudi.addEventListener("proceed", fetchHandler);
1138
- weiwudi.addEventListener("finish", fetchHandler);
1139
- weiwudi.addEventListener("stop", fetchHandler);
1140
- // Check if weiwudi dispatches 'canceled'. Based on my read, it does dispatch e.data.type
1141
- // And weiwudi_gw_logic sends type: 'canceled'.
1142
- weiwudi.addEventListener("canceled", fetchHandler);
1143
-
1144
- const removeListeners = () => {
1145
- weiwudi.removeEventListener("proceed", fetchHandler);
1146
- weiwudi.removeEventListener("finish", fetchHandler);
1147
- weiwudi.removeEventListener("stop", fetchHandler);
1148
- weiwudi.removeEventListener("canceled", fetchHandler);
1149
- modalBase.removeEventListener("hidden.bs.modal", removeListeners);
1150
- };
1151
- modalBase.addEventListener("hidden.bs.modal", removeListeners);
1152
-
1153
- const newElem = cacheFetch.cloneNode(true);
1154
- cacheFetch.parentNode!.replaceChild(newElem, cacheFetch);
1155
- // Initial update handled by showStats -> updateButtons
1156
- cacheFetch = newElem as HTMLButtonElement;
1157
-
1158
- cacheFetch.addEventListener("click", async () => {
1159
- if (isFetching) {
1160
- await weiwudi.cancel();
1161
- } else {
1162
- isFetching = true; // Set immediately to update UI
1163
- updateButtons();
1164
- try {
1165
- await weiwudi.fetchAll();
1166
- } catch {
1167
- isFetching = false;
1168
- updateButtons();
1169
- }
1170
- }
1171
- await showStats();
1172
- });
1173
-
1174
- const newElem2 = cacheDelete.cloneNode(true);
1175
- cacheDelete.parentNode!.replaceChild(newElem2, cacheDelete);
1176
- cacheDelete = newElem2 as HTMLButtonElement;
1177
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1178
- const t = (ui.core as any).t;
1179
- cacheDelete.innerHTML =
1180
- (t ? t.call(ui.core, "html.cache_delete") : undefined) ||
1181
- ui.core!.translate("html.cache_delete") ||
1182
- "Clear";
1183
-
1184
- cacheDelete.addEventListener("click", async () => {
1185
- if (isFetching) return; // Should be disabled, but safety check
1186
- await weiwudi.clean();
1187
- await showStats();
1188
- });
1189
- } else {
1190
- cacheDiv.style.display = "none";
1191
- }
1192
-
1193
- modal.show();
1194
- } else if (control === "border") {
1195
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1196
- ui.setShowBorder(!(ui.core!.stateBuffer as any).showBorder);
1197
- } else if (control === "hideMarker") {
1198
- const current = mapDiv.classList.contains("hide-marker");
1199
- ui.setHideMarker(!current);
1200
- }
1201
- ui.updateUrl();
1202
- });
1203
- });
1204
- }
1205
-
1206
- function initDom(ui: MaplatUi, appOption: MaplatAppOption) {
1207
- // Inject Custom Toast Styles
1208
- const style = document.createElement("style");
1209
- style.innerHTML = `
1210
- .custom-toast {
1211
- visibility: hidden;
1212
- min-width: 250px;
1213
- margin-left: -125px;
1214
- background-color: #333;
1215
- color: #fff;
1216
- text-align: center;
1217
- border-radius: 2px;
1218
- padding: 16px;
1219
- position: fixed;
1220
- z-index: 9999;
1221
- left: 50%;
1222
- bottom: 30px;
1223
- font-size: 17px;
1224
- opacity: 0;
1225
- transition: opacity 0.3s;
1226
- }
1227
- .custom-toast.show {
1228
- visibility: visible;
1229
- opacity: 1;
1230
- }
1231
- `;
1232
- document.head.appendChild(style);
1233
-
1234
- let pwaManifest = appOption.pwaManifest;
1235
- let pwaWorker = appOption.pwaWorker;
1236
- let pwaScope = appOption.pwaScope;
1237
-
1238
- // Add UI HTML Element
1239
- let newElems = createElement(`<d c="ol-control map-title"><s></s></d>
1240
- <d c="swiper-container ol-control base-swiper prevent-default-ui">
1241
- <d c="swiper-wrapper"> </d>
1242
- <d c="swiper-button-next base-next swiper-button-white"> </d>
1243
- <d c="swiper-button-prev base-prev swiper-button-white"> </d>
1244
- </d>
1245
- <d c="swiper-container ol-control overlay-swiper prevent-default-ui">
1246
- <d c="swiper-wrapper"> </d>
1247
- <d c="swiper-button-next overlay-next swiper-button-white"> </d>
1248
- <d c="swiper-button-prev overlay-prev swiper-button-white"> </d>
1249
- </d> `);
1250
- for (let i = newElems.length - 1; i >= 0; i--) {
1251
- ui.core!.mapDivDocument!.insertBefore(
1252
- newElems[i],
1253
- ui.core!.mapDivDocument!.firstChild
1254
- );
1255
- }
1256
-
1257
- newElems = createElement(`<d c="modal modalBase" tabindex="-1" role="dialog"
1258
- aria-labelledby="staticModalLabel" aria-hidden="true" data-show="true" data-keyboard="false"
1259
- data-backdrop="static">
1260
- <d c="modal-dialog">
1261
- <d c="modal-content">
1262
- <d c="modal-header">
1263
- <button type="button" c="close" data-dismiss="modal">
1264
- <s aria-hidden="true">&#215;</s><s c="sr-only" din="html.close"></s>
1265
- </button>
1266
- <h4 c="modal-title">
1267
-
1268
- <s c="modal_title"></s>
1269
- <s c="modal_load_title"></s>
1270
- <s c="modal_gpsW_title" din="html.acquiring_gps"></s>
1271
- <s c="modal_help_title" din="html.help_title"></s>
1272
- <s c="modal_share_title" din="html.share_title"></s>
1273
- <s c="modal_marker_list_title" din="html.marker_list_title"></s>
1274
-
1275
- </h4>
1276
- </d>
1277
- <d c="modal-body">
1278
-
1279
- <d c="modal_help_content">
1280
- <d c="help_content">
1281
- <s dinh="html.help_using_maplat"></s>
1282
- <p c="col-xs-12 help_img"><img src="${pointer["fullscreen.png"]}"></p>
1283
- <h4 din="html.help_operation_title"></h4>
1284
- <p dinh="html.help_operation_content" c="recipient"></p>
1285
- <h4 din="html.help_selection_title"></h4>
1286
- <p dinh="html.help_selection_content" c="recipient"></p>
1287
- <h4 din="html.help_gps_title"></h4>
1288
- <p dinh="html.help_gps_content" c="recipient"></p>
1289
- <h4 din="html.help_poi_title"></h4>
1290
- <p dinh="html.help_poi_content" c="recipient"></p>
1291
- <h4 din="html.help_etc_title"></h4>
1292
- <ul>
1293
- <li dinh="html.help_etc_attr" c="recipient"></li>
1294
- <li dinh="html.help_etc_help" c="recipient"></li>
1295
- <s c="share_help"><li dinh="html.help_share_help" c="recipient"></li></s>
1296
- <s c="border_help"><li dinh="html.help_etc_border" c="recipient"></li></s>
1297
- <s c="hide_marker_help"><li dinh="html.help_etc_hide_marker" c="recipient"></li></s>
1298
- <s c="marker_list_help"><li dinh="html.help_etc_marker_list" c="recipient"></li></s>
1299
- <li dinh="html.help_etc_slider" c="recipient"></li>
1300
- </ul>
1301
- <p><a href="https://www.maplat.jp/" target="_blank">Maplat</a>
1302
- © 2015- Kohei Otsuka, Code for History</p>
1303
- </d>
1304
- </d>
1305
-
1306
- <d c="modal_poi_content">
1307
- <d c="poi_web_div"></d>
1308
- <d c="poi_share_buttons recipient row">
1309
- <d c="form-group col-xs-4 text-center"><button title="Copy to clipboard" class="share btn btn-light" data="cp_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M224 0c-35.3 0-64 28.7-64 64V96H96c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H288c35.3 0 64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H224zM288 384H96V160H224c0-17.7 14.3-32 32-32h64V256c0 17.7 14.3 32 32 32h96V384H288z"/></svg>&nbsp;<small din="html.share_copy"></small></button></d>
1310
- <d c="form-group col-xs-4 text-center"><button title="Twitter" class="share btn btn-light" data="tw_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>&nbsp;<small>Twitter</small></button></d>
1311
- <d c="form-group col-xs-4 text-center"><button title="Facebook" class="share btn btn-light" data="fb_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256C504 119 393 8 256 8S8 119 8 256c0 121.3 87.1 222.4 203 240.5V327.9h-61v-71.9h61V203c0-60.8 35.8-93.7 89.2-93.7 25.5 0 50.4 1.8 56.1 2.6v62.4h-35.4c-29.5 0-37.4 18.2-37.4 42.1v59.6h68.9l-11 71.9h-57.9V496.5C416.9 478.4 504 377.3 504 256z"/></svg>&nbsp;<small>Facebook</small></button></d>
1312
- </d>
1313
- <d c="qr_view_poi center-block" style="width:128px;"></d>
1314
- <d c="modal_share_poi"></d>
1315
- <p><img src="" height="0px" width="0px"></p>
1316
- </d>
1317
-
1318
- <d c="modal_share_content">
1319
- <h4 din="html.share_app_title"></h4>
1320
- <d id="___maplat_app_toast_${ui.html_id_seed}"></d>
1321
- <d c="recipient row">
1322
- <d c="form-group col-xs-4 text-center"><button title="Copy to clipboard" class="share btn btn-light" data="cp_app"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M224 0c-35.3 0-64 28.7-64 64V96H96c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H288c35.3 0 64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H224zM288 384H96V160H224c0-17.7 14.3-32 32-32h64V256c0 17.7 14.3 32 32 32h96V384H288z"/></svg>&nbsp;<small din="html.share_copy"></small></button></d>
1323
- <d c="form-group col-xs-4 text-center"><button title="Twitter" class="share btn btn-light" data="tw_app"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>&nbsp;<small>Twitter</small></button></d>
1324
- <d c="form-group col-xs-4 text-center"><button title="Facebook" class="share btn btn-light" data="fb_app"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256C504 119 393 8 256 8S8 119 8 256c0 121.3 87.1 222.4 203 240.5V327.9h-61v-71.9h61V203c0-60.8 35.8-93.7 89.2-93.7 25.5 0 50.4 1.8 56.1 2.6v62.4h-35.4c-29.5 0-37.4 18.2-37.4 42.1v59.6h68.9l-11 71.9h-57.9V496.5C416.9 478.4 504 377.3 504 256z"/></svg>&nbsp;<small>Facebook</small></button></d>
1325
- </d>
1326
- <d c="qr_app center-block" style="width:128px;"></d>
1327
- <d c="modal_share_state">
1328
- <h4 din="html.share_state_title"></h4>
1329
- <d id="___maplat_view_toast_${ui.html_id_seed}"></d>
1330
- <d c="recipient row">
1331
- <d c="form-group col-xs-4 text-center"><button title="Copy to clipboard" c="share btn btn-light" data="cp_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M224 0c-35.3 0-64 28.7-64 64V96H96c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H288c35.3 0 64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H224zM288 384H96V160H224c0-17.7 14.3-32 32-32h64V256c0 17.7 14.3 32 32 32h96V384H288z"/></svg>&nbsp;<small din="html.share_copy"></small></button></d>
1332
- <d c="form-group col-xs-4 text-center"><button title="Twitter" c="share btn btn-light" data="tw_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>&nbsp;<small>Twitter</small></button></d>
1333
- <d c="form-group col-xs-4 text-center"><button title="Facebook" c="share btn btn-light" data="fb_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256C504 119 393 8 256 8S8 119 8 256c0 121.3 87.1 222.4 203 240.5V327.9h-61v-71.9h61V203c0-60.8 35.8-93.7 89.2-93.7 25.5 0 50.4 1.8 56.1 2.6v62.4h-35.4c-29.5 0-37.4 18.2-37.4 42.1v59.6h68.9l-11 71.9h-57.9V496.5C416.9 478.4 504 377.3 504 256z"/></svg>&nbsp;<small>Facebook</small></button></d>
1334
- </d>
1335
- <d c="qr_view center-block" style="width:128px;"></d>
1336
- </d>
1337
- <p><img src="" height="0px" width="0px"></p>
1338
- </d>
1339
-
1340
- <d c="modal_map_content">
1341
- ${META_KEYS.map(key => {
1342
- if (key == "title" || key == "officialTitle") return "";
1343
- return `<d c="recipients ${key}_div"><dl c="dl-horizontal">
1344
- <dt din="html.${key}"></dt>
1345
- <dd c="${key}_dd"></dd>
1346
- </dl></d> `;
1347
- }).join("")}
1348
- <d c="recipients modal_cache_content"><dl c="dl-horizontal">
1349
- <dt din="html.cache_handle"></dt>
1350
- <dd><s c="cache_size"></s></dd>
1351
- <dt></dt>
1352
- <dd><s c="pull-right"><button c="cache_fetch btn btn-default" href="#" din="html.cache_fetch"></button>
1353
- <button c="cache_delete btn btn-default" href="#" din="html.cache_delete"></button></s></dd>
1354
- </dl></d>
1355
- </d>
1356
-
1357
- <d c="modal_load_content">
1358
- <p c="recipient"><img src="${pointer["loading.png"]}"><s din="html.app_loading_body"></s></p>
1359
- <d c="splash_div hide row"><p c="col-xs-12 poi_img"><img c="splash_img" src=""></p></d>
1360
- <p><img src="" height="0px" width="0px"></p>
1361
- </d>
1362
-
1363
- <d c="modal_marker_list_content">
1364
- <ul c="list-group"></ul>
1365
- </d>
1366
-
1367
- <p c="modal_gpsD_content" c="recipient"></p>
1368
- <p c="modal_gpsW_content" c="recipient"></p>
1369
-
1370
- </d>
1371
- </d>
1372
- </d>
1373
- </d> `);
1374
-
1375
- for (let i = newElems.length - 1; i >= 0; i--) {
1376
- ui.core!.mapDivDocument!.insertBefore(
1377
- newElems[i],
1378
- ui.core!.mapDivDocument!.firstChild
1379
- );
1380
- }
1381
-
1382
- // PWA
1383
- if (pwaManifest) {
1384
- if (pwaManifest === true) {
1385
- pwaManifest = `./pwa/${ui.core!.appid}_manifest.json`;
1386
- }
1387
- if (!pwaWorker) {
1388
- pwaWorker = "./service-worker.js";
1389
- }
1390
- if (!pwaScope) {
1391
- pwaScope = "./";
1392
- }
1393
-
1394
- const head = document.querySelector("head");
1395
- if (head) {
1396
- if (!head.querySelector('link[rel="manifest"]')) {
1397
- head.appendChild(
1398
- createElement(`<link rel="manifest" href="${pwaManifest}">`)[0]
1399
- );
1400
- }
1401
- }
1402
- try {
1403
- Weiwudi.registerSW(pwaWorker, { scope: pwaScope });
1404
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1405
- } catch (_e) {} // eslint-disable-line no-empty
1406
-
1407
- if (head && !head.querySelector('link[rel="apple-touch-icon"]')) {
1408
- fetch(pwaManifest)
1409
- .then(response => response.json())
1410
- .then(value => {
1411
- if (value.icons) {
1412
- value.icons.forEach((icon: { src: string; sizes: string }) => {
1413
- const src = absoluteUrl(pwaManifest as string, icon.src);
1414
- const sizes = icon.sizes;
1415
- const tag = `<link rel="apple-touch-icon" sizes="${sizes}" href="${src}">`;
1416
- head.appendChild(createElement(tag)[0]);
1417
- });
1418
- }
1419
- })
1420
- .catch(err => {
1421
- console.error("Failed to fetch PWA manifest:", err);
1422
- });
1423
- }
1424
- }
1425
- }
1
+ import { MaplatApp as Core, MaplatApp } from "@maplat/core";
2
+ import pointer from "./pointer_images";
3
+ import { Swiper } from "./swiper_ex";
4
+ import { Navigation, Pagination } from "swiper";
5
+ import "swiper/swiper-bundle.css";
6
+ import {
7
+ SliderNew,
8
+ Copyright,
9
+ CompassRotate,
10
+ Zoom,
11
+ SetGPS,
12
+ GoHome,
13
+ Maplat,
14
+ Share,
15
+ Border,
16
+ HideMarker,
17
+ MarkerList
18
+ } from "./maplat_control";
19
+ import ContextMenu from "./contextmenu";
20
+ import Weiwudi from "@c4h/weiwudi";
21
+ import absoluteUrl from "./absolute_url";
22
+ import * as QRCode from "qrcode";
23
+ import {
24
+ createElement,
25
+ ellips,
26
+ encBytes,
27
+ isBasemap,
28
+ prepareModal
29
+ } from "./ui_utils";
30
+
31
+ import { poiWebControl } from "./ui_marker";
32
+
33
+ import type { MaplatUi } from "./index";
34
+ import type { MaplatAppOption } from "./types";
35
+ import i18n from "i18next";
36
+ import i18nHttpBackend from "i18next-http-backend";
37
+
38
+ Swiper.use([Navigation, Pagination]);
39
+
40
+ export const META_KEYS = [
41
+ "title",
42
+ "officialTitle",
43
+ "author",
44
+ "epoch",
45
+ "createdAt",
46
+ "era",
47
+ "contributor",
48
+ "mapper",
49
+ "license",
50
+ "dataLicense",
51
+ "attr",
52
+ "dataAttr",
53
+ "reference",
54
+ "description"
55
+ ];
56
+
57
+ async function i18nLoader(ui: MaplatUi) {
58
+ return new Promise<void>((resolve, _reject) => {
59
+ const translib = i18n.use(i18nHttpBackend);
60
+ translib.init(
61
+ {
62
+ lng: "ja", //ui.core!.lang,
63
+ fallbackLng: ["en"],
64
+ backend: {
65
+ loadPath: "assets/locales/{{lng}}/{{ns}}.json"
66
+ }
67
+ },
68
+ (_err, t) => {
69
+ ui._t = t;
70
+ ui.i18n = i18n;
71
+ resolve();
72
+ }
73
+ );
74
+ });
75
+ }
76
+
77
+ export async function uiInit(ui: MaplatUi, appOption: MaplatAppOption) {
78
+ await i18nLoader(ui);
79
+ ui.core = new Core(appOption);
80
+ if (appOption.icon) {
81
+ (pointer as Record<string, string>)["defaultpin.png"] = appOption.icon;
82
+ }
83
+
84
+ if (appOption.restore) {
85
+ ui.setShowBorder(appOption.restore.showBorder || false);
86
+ if (appOption.restore.hideMarker) {
87
+ ui.core!.waitReady.then(() => {
88
+ ui.setHideMarker(appOption.restore!.hideMarker);
89
+ });
90
+ }
91
+ if (appOption.restore.openedMarker) {
92
+ console.log(appOption.restore.openedMarker);
93
+ ui.core!.waitReady.then(() => {
94
+ console.log(`Timeout ${appOption.restore!.openedMarker} `);
95
+ ui.handleMarkerActionById(appOption.restore!.openedMarker!);
96
+ });
97
+ }
98
+ } else if (appOption.restoreSession) {
99
+ const lastEpoch = parseInt(
100
+ String(localStorage.getItem("epoch") || "0"),
101
+ 10
102
+ );
103
+ const currentTime = Math.floor(new Date().getTime() / 1000);
104
+ if (lastEpoch && currentTime - lastEpoch < 3600) {
105
+ ui.setShowBorder(
106
+ !!parseInt(String(localStorage.getItem("showBorder") || "0"), 10)
107
+ );
108
+ }
109
+ if (ui.core!.initialRestore.hideMarker) {
110
+ ui.core!.waitReady.then(() => {
111
+ ui.setHideMarker(true);
112
+ });
113
+ }
114
+ } else {
115
+ ui.setShowBorder(false);
116
+ }
117
+
118
+ ui.enablePoiHtmlNoScroll = appOption.enablePoiHtmlNoScroll || false;
119
+ if (appOption.enableShare) {
120
+ ui.core!.mapDivDocument!.classList.add("enable_share");
121
+ ui.enableShare = true;
122
+ }
123
+ if (appOption.enableHideMarker) {
124
+ ui.core!.mapDivDocument!.classList.add("enable_hide_marker");
125
+ ui.enableHideMarker = true;
126
+ }
127
+ if (appOption.enableBorder) {
128
+ ui.core!.mapDivDocument!.classList.add("enable_border");
129
+ ui.enableBorder = true;
130
+ }
131
+ if (appOption.enableMarkerList) {
132
+ ui.core!.mapDivDocument!.classList.add("enable_marker_list");
133
+ ui.enableMarkerList = true;
134
+ }
135
+ if (appOption.disableNoimage) {
136
+ ui.disableNoimage = true;
137
+ }
138
+ if (appOption.stateUrl) {
139
+ ui.core!.mapDivDocument!.classList.add("state_url");
140
+ }
141
+ if (appOption.alwaysGpsOn) {
142
+ ui.alwaysGpsOn = true;
143
+ }
144
+ if (ui.core!.enableCache) {
145
+ ui.core!.mapDivDocument!.classList.add("enable_cache");
146
+ }
147
+ if ("ontouchstart" in window) {
148
+ ui.core!.mapDivDocument!.classList.add("ol-touch");
149
+ ui.isTouch = true;
150
+ }
151
+ if (appOption.mobileIF) {
152
+ appOption.debug = true;
153
+ }
154
+ if (appOption.appEnvelope) {
155
+ ui.appEnvelope = true;
156
+ }
157
+
158
+ initDom(ui, appOption);
159
+ initModalHandlers(ui, appOption);
160
+ initMapEventListeners(ui);
161
+ initGpsHandlers(ui, appOption);
162
+ }
163
+
164
+ function initGpsHandlers(ui: MaplatUi, appOption: MaplatAppOption) {
165
+ const enableOutOfMap = !appOption.presentationMode;
166
+ const mapDiv = ui.core!.mapDivDocument!;
167
+ const modalBase = mapDiv.querySelector(".modalBase")!;
168
+ const modalTitle = mapDiv.querySelector(".modal_title") as HTMLElement;
169
+ const modalGpsDContent = mapDiv.querySelector(
170
+ ".modal_gpsD_content"
171
+ ) as HTMLElement;
172
+
173
+ ui.core!.addEventListener("outOfMap", (_evt: unknown) => {
174
+ console.log("Event: outOfMap");
175
+ if (enableOutOfMap) {
176
+ modalTitle.innerText = ui.t("app.out_of_map") || "";
177
+ modalGpsDContent.innerText = ui.t("app.out_of_map_area") || "";
178
+ ui.modalSetting("gpsD");
179
+ prepareModal(modalBase, { root: mapDiv }).show();
180
+ }
181
+ });
182
+
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
+ ui.core!.addEventListener("gps_error", (evt: any) => {
185
+ console.log("GPS Error:", evt);
186
+ const errorMap: Record<string, string> = {
187
+ user_gps_deny: "app.user_gps_deny",
188
+ gps_miss: "app.gps_miss",
189
+ gps_timeout: "app.gps_timeout"
190
+ };
191
+
192
+ if (!ui.core) return;
193
+ modalTitle.innerText = ui.t("app.gps_error") || "";
194
+ modalGpsDContent.innerText =
195
+ ui.t(errorMap[evt.detail] || "app.gps_error") || "";
196
+ ui.modalSetting("gpsD");
197
+ prepareModal(modalBase, {
198
+ root: mapDiv
199
+ }).show();
200
+ });
201
+
202
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
203
+ ui.core!.addEventListener("gps_result", (evt: any) => {
204
+ console.log("GPS Result:", evt);
205
+ if (evt.detail && evt.detail.error) {
206
+ const error = evt.detail.error;
207
+ if (error === "gps_off") {
208
+ ui.lastGPSError = undefined;
209
+ return;
210
+ }
211
+
212
+ ui.lastGPSError = error;
213
+ if (ui.alwaysGpsOn && error === "gps_out") return;
214
+
215
+ if (!ui.core) return;
216
+
217
+ const modal = prepareModal(modalBase, {
218
+ root: mapDiv
219
+ });
220
+
221
+ if (error === "gps_out") {
222
+ modalTitle.innerText = ui.t("app.out_of_map") || "";
223
+ modalGpsDContent.innerText = ui.t("app.out_of_map_area") || "";
224
+ } else {
225
+ const errorMap: Record<string, string> = {
226
+ user_gps_deny: "app.user_gps_deny",
227
+ gps_miss: "app.gps_miss",
228
+ gps_timeout: "app.gps_timeout"
229
+ };
230
+
231
+ modalTitle.innerText = ui.t("app.gps_error") || "";
232
+ modalGpsDContent.innerText =
233
+ ui.t(errorMap[error] || "app.gps_error") || "";
234
+ }
235
+
236
+ ui.modalSetting("gpsD");
237
+ modal.show();
238
+ } else {
239
+ ui.lastGPSError = undefined;
240
+ }
241
+ });
242
+ }
243
+
244
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
245
+ function initSwipers(ui: MaplatUi, sources: any[]) {
246
+ const colors = [
247
+ "maroon",
248
+ "deeppink",
249
+ "indigo",
250
+ "olive",
251
+ "royalblue",
252
+ "red",
253
+ "hotpink",
254
+ "green",
255
+ "yellow",
256
+ "navy",
257
+ "saddlebrown",
258
+ "fuchsia",
259
+ "darkslategray",
260
+ "yellowgreen",
261
+ "blue",
262
+ "mediumvioletred",
263
+ "purple",
264
+ "lime",
265
+ "darkorange",
266
+ "teal",
267
+ "crimson",
268
+ "darkviolet",
269
+ "darkolivegreen",
270
+ "steelblue",
271
+ "aqua"
272
+ ];
273
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
274
+ const appBbox: any[] = [];
275
+ let cIndex = 0;
276
+
277
+ for (let i = 0; i < sources.length; i++) {
278
+ const source = sources[i];
279
+ if (source.envelope) {
280
+ if (ui.appEnvelope) {
281
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
282
+ source.envelope.geometry.coordinates[0].map((xy: any) => {
283
+ if (appBbox.length === 0) {
284
+ appBbox[0] = appBbox[2] = xy[0];
285
+ appBbox[1] = appBbox[3] = xy[1];
286
+ } else {
287
+ if (xy[0] < appBbox[0]) appBbox[0] = xy[0];
288
+ if (xy[0] > appBbox[2]) appBbox[2] = xy[0];
289
+
290
+ if (xy[1] < appBbox[1]) appBbox[1] = xy[1];
291
+ if (xy[1] > appBbox[3]) appBbox[3] = xy[1];
292
+ }
293
+ });
294
+ }
295
+ source.envelopeColor = colors[cIndex];
296
+ cIndex++;
297
+ if (cIndex === colors.length) cIndex = 0;
298
+
299
+ const xys = source.envelope.geometry.coordinates[0];
300
+ source.envelopeAreaIndex = ui.areaIndex(xys);
301
+ }
302
+ }
303
+ if (ui.appEnvelope) console.log(`This app's envelope is: ${appBbox}`);
304
+
305
+ if (ui.splashPromise) {
306
+ ui.splashPromise.then(() => {
307
+ const modalElm = ui.core!.mapDivDocument!.querySelector(".modalBase")!;
308
+ prepareModal(modalElm, { root: ui.core!.mapDivDocument! }).hide();
309
+ });
310
+ }
311
+
312
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
313
+ const baseSources: any[] = [];
314
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
315
+ const overlaySources: any[] = [];
316
+ sources.forEach(source => {
317
+ if (isBasemap(source)) {
318
+ baseSources.push(source);
319
+ } else {
320
+ overlaySources.push(source);
321
+ }
322
+ });
323
+
324
+ const baseSwiper = (ui.baseSwiper = new Swiper(".base-swiper", {
325
+ slidesPerView: 2,
326
+ spaceBetween: 15,
327
+ breakpoints: {
328
+ 480: {
329
+ slidesPerView: 1.4,
330
+ spaceBetween: 10
331
+ }
332
+ },
333
+ centeredSlides: true,
334
+ threshold: 2,
335
+ preventClicks: true,
336
+ preventClicksPropagation: true,
337
+ observer: true,
338
+ observeParents: true,
339
+ loop: baseSources.length >= 2,
340
+ navigation: {
341
+ nextEl: ".base-next",
342
+ prevEl: ".base-prev"
343
+ }
344
+ }));
345
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
346
+ baseSwiper.on("click", (_e: any) => {
347
+ if (!baseSwiper.clickedSlide) return;
348
+ const slide = baseSwiper.clickedSlide;
349
+ ui.core!.changeMap(slide.getAttribute("data")!);
350
+ delete ui._selectCandidateSources;
351
+ baseSwiper.setSlideIndexAsSelected(
352
+ parseInt(slide.getAttribute("data-swiper-slide-index") || "0", 10)
353
+ );
354
+ });
355
+ if (baseSources.length < 2) {
356
+ ui.core!.mapDivDocument!.querySelector(".base-swiper")!.classList.add(
357
+ "single-map"
358
+ );
359
+ }
360
+
361
+ const overlaySwiper = (ui.overlaySwiper = new Swiper(".overlay-swiper", {
362
+ slidesPerView: 2,
363
+ spaceBetween: 15,
364
+ breakpoints: {
365
+ 480: {
366
+ slidesPerView: 1.4,
367
+ spaceBetween: 10
368
+ }
369
+ },
370
+ centeredSlides: true,
371
+ threshold: 2,
372
+ preventClicks: true,
373
+ preventClicksPropagation: true,
374
+ observer: true,
375
+ observeParents: true,
376
+ loop: overlaySources.length >= 2,
377
+ navigation: {
378
+ nextEl: ".overlay-next",
379
+ prevEl: ".overlay-prev"
380
+ }
381
+ }));
382
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
383
+ overlaySwiper.on("click", (_e: any) => {
384
+ if (!overlaySwiper.clickedSlide) return;
385
+ const slide = overlaySwiper.clickedSlide;
386
+ ui.core!.changeMap(slide.getAttribute("data")!);
387
+ delete ui._selectCandidateSources;
388
+ overlaySwiper.setSlideIndexAsSelected(
389
+ parseInt(slide.getAttribute("data-swiper-slide-index") || "0", 10)
390
+ );
391
+ });
392
+ if (overlaySources.length < 2) {
393
+ ui.core!.mapDivDocument!.querySelector(".overlay-swiper")!.classList.add(
394
+ "single-map"
395
+ );
396
+ }
397
+
398
+ baseSources.forEach(source => {
399
+ const thumbKey = source.thumbnail ? source.thumbnail.split("/").pop() : "";
400
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
401
+ const thumbUrl = (pointer as any)[thumbKey] || source.thumbnail;
402
+ baseSwiper.appendSlide(
403
+ `<div class="swiper-slide" data="${source.mapID}">` +
404
+ `<img crossorigin="anonymous" src="${
405
+ thumbUrl
406
+ }"><div> ${ui.translate!(source.label)}</div> </div> `
407
+ );
408
+ });
409
+ overlaySources.forEach(source => {
410
+ const colorCss = source.envelope ? ` ${source.envelopeColor}` : "";
411
+ const thumbKey = source.thumbnail ? source.thumbnail.split("/").pop() : "";
412
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
413
+ const thumbUrl = (pointer as any)[thumbKey] || source.thumbnail;
414
+ overlaySwiper.appendSlide(
415
+ `<div class="swiper-slide${colorCss}" data="${source.mapID}">` +
416
+ `<img crossorigin="anonymous" src="${
417
+ thumbUrl
418
+ }"><div> ${ui.translate!(source.label)}</div> </div> `
419
+ );
420
+ });
421
+
422
+ overlaySwiper.on("slideChange", () => {
423
+ ui.updateEnvelope();
424
+ });
425
+
426
+ baseSwiper.slideToLoop(0);
427
+ overlaySwiper.slideToLoop(0);
428
+ ellips(ui.core!.mapDivDocument!);
429
+ }
430
+
431
+ function initMapEventListeners(ui: MaplatUi) {
432
+ ui.core!.addEventListener("mapChanged", (evt: unknown) => {
433
+ const map = (evt as CustomEvent).detail;
434
+
435
+ ui.baseSwiper.setSlideMapID(map.mapID);
436
+ ui.overlaySwiper.setSlideMapID(map.mapID);
437
+
438
+ const title = map.officialTitle || map.title || map.label;
439
+ (
440
+ ui.core!.mapDivDocument!.querySelector(".map-title span") as HTMLElement
441
+ ).innerText = ui.translate!(title) || "";
442
+
443
+ if (ui.checkOverlayID(map.mapID)) {
444
+ ui.sliderNew.setEnable(true);
445
+ } else {
446
+ ui.sliderNew.setEnable(false);
447
+ }
448
+ const transparency = ui.sliderNew.get("slidervalue") * 100;
449
+ ui.core!.mapObject.setTransparency(transparency);
450
+
451
+ ui.updateEnvelope();
452
+ ui.updateUrl();
453
+ });
454
+
455
+ ui.core!.addEventListener("poi_number", (evt: unknown) => {
456
+ const number = (evt as CustomEvent).detail;
457
+ if (number) {
458
+ ui.core!.mapDivDocument!.classList.remove("no_poi");
459
+ } else {
460
+ ui.core!.mapDivDocument!.classList.add("no_poi");
461
+ }
462
+ });
463
+
464
+ ui.core!.addEventListener("sourceLoaded", (evt: unknown) => {
465
+ const sources = (evt as CustomEvent).detail;
466
+ initSwipers(ui, sources);
467
+ });
468
+
469
+ ui.core!.waitReady.then(() => {
470
+ // Capture pointerdown at viewport level to ensure we get pixel before any stopPropagation
471
+
472
+ ui.core!.mapObject.getViewport().addEventListener(
473
+ "pointerdown",
474
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
475
+ (evt: any) => {
476
+ ui.lastClickPixel = ui.core!.mapObject.getEventPixel(evt);
477
+ ui.lastClickCoordinate = ui.core!.mapObject.getCoordinateFromPixel(
478
+ ui.lastClickPixel
479
+ );
480
+ },
481
+ true
482
+ );
483
+ });
484
+
485
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
486
+ ui.core!.addEventListener("clickMarkers", (evt: any) => {
487
+ const data = evt.detail;
488
+ if (data.length === 1) {
489
+ ui.handleMarkerAction(data[0]);
490
+ } else {
491
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
492
+ const list: any[] = [];
493
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
494
+ data.forEach((datum: any) => {
495
+ list.push({
496
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
497
+ icon: datum.icon || (pointer as any)["defaultpin.png"],
498
+ text: ui.translate!(datum.name),
499
+ callback: () => {
500
+ ui.handleMarkerAction(datum);
501
+ }
502
+ });
503
+ });
504
+ ui.showContextMenu(list);
505
+ }
506
+ });
507
+
508
+ ui.core!.waitReady.then(() => {
509
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
510
+ ui.core!.mapObject.on("moveend", (_evt: any) => {
511
+ ui.updateUrl();
512
+ });
513
+ });
514
+ }
515
+
516
+ function initModalHandlers(ui: MaplatUi, appOption: MaplatAppOption) {
517
+ let cachedMarkerListMapID: string | undefined;
518
+ const mapDiv = ui.core!.mapDivDocument!;
519
+ const modalBase = mapDiv.querySelector(".modalBase") as HTMLElement;
520
+
521
+ const restoreTransparency =
522
+ ui.core!.initialRestore.transparency ||
523
+ (appOption.restore ? appOption.restore.transparency : undefined);
524
+ const enableSplash = !ui.core!.initialRestore.mapID;
525
+
526
+ // Delegated event listener for share buttons
527
+ mapDiv.addEventListener("click", (evt: Event) => {
528
+ const target = evt.target as HTMLElement;
529
+ const btn = target.closest(".share") || target.closest(".share_button");
530
+ if (!btn) return;
531
+
532
+ console.log("Share button clicked:", btn);
533
+ const cmd = btn.getAttribute("data");
534
+ if (!cmd) return;
535
+ const cmds = cmd.split("_");
536
+ const uri = ui.getShareUrl(cmds[1] || "app");
537
+
538
+ console.log("Share URI:", uri);
539
+
540
+ if (cmds[0] === "cp") {
541
+ const bodyElm = document.querySelector("body")!;
542
+ const message = ui.t ? ui.t("app.copy_toast") : "URL Copied";
543
+ if (navigator.clipboard) {
544
+ navigator.clipboard.writeText(uri).then(() => {
545
+ ui.showToast(message, btn as HTMLElement);
546
+ });
547
+ } else {
548
+ const copyFrom = document.createElement("textarea");
549
+ copyFrom.textContent = uri;
550
+ bodyElm.appendChild(copyFrom);
551
+ copyFrom.select();
552
+ document.execCommand("copy");
553
+ bodyElm.removeChild(copyFrom);
554
+ ui.showToast(message, btn as HTMLElement);
555
+ }
556
+ } else if (cmds[0] === "tw") {
557
+ const text = document.title;
558
+ const twuri = `https://twitter.com/intent/tweet?url=${encodeURIComponent(uri)}&text=${encodeURIComponent(text)}&hashtags=Maplat`;
559
+ window.open(twuri, "_blank");
560
+ } else if (cmds[0] === "fb") {
561
+ const fburi = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(uri)}&display=popup&ref=plugin&src=like&kid_directed_site=0`;
562
+ window.open(
563
+ fburi,
564
+ "_blank",
565
+ "width=650,height=450,menubar=no,toolbar=no,scrollbars=yes"
566
+ );
567
+ } else if (cmds[0] === "qr") {
568
+ const qrDiv =
569
+ mapDiv.querySelector(".qr_view_poi") ||
570
+ mapDiv.querySelector(".qr_view");
571
+
572
+ if (qrDiv) {
573
+ QRCode.toCanvas(
574
+ uri,
575
+ { width: 128, margin: 1 },
576
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
577
+ (err: any, canvas: any) => {
578
+ if (!err) {
579
+ qrDiv.innerHTML = "";
580
+ qrDiv.appendChild(canvas);
581
+ }
582
+ }
583
+ );
584
+ }
585
+ }
586
+ });
587
+
588
+ const prevDefs = mapDiv.querySelectorAll(".prevent-default-ui");
589
+ prevDefs.forEach(target => {
590
+ target.addEventListener("touchstart", (evt: Event) => {
591
+ evt.preventDefault();
592
+ });
593
+ });
594
+
595
+ ui.core!.addEventListener("uiPrepare", (_evt: unknown) => {
596
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
597
+ const imageExtractor = function (text: any) {
598
+ const regexp = /\$\{([a-zA-Z0-9_\.\/\-]+)\}/g; // eslint-disable-line no-useless-escape
599
+ let ret = text;
600
+ let match;
601
+ while ((match = regexp.exec(text)) != null) {
602
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
603
+ ret = ret.replace(match[0], (pointer as any)[match[1]]);
604
+ }
605
+ return ret;
606
+ };
607
+
608
+ let i18nTargets = mapDiv.querySelectorAll("[data-i18n], [din]");
609
+ i18nTargets.forEach(target => {
610
+ const key =
611
+ target.getAttribute("data-i18n") || target.getAttribute("din");
612
+ (target as HTMLElement).innerText = imageExtractor(ui.t(key as string));
613
+ });
614
+ i18nTargets = mapDiv.querySelectorAll("[data-i18n-html], [dinh]");
615
+ i18nTargets.forEach(target => {
616
+ const key =
617
+ target.getAttribute("data-i18n-html") || target.getAttribute("dinh");
618
+ target.innerHTML = imageExtractor(ui.t(key as string));
619
+ });
620
+ // Explicitly fix app_loading_body with a more robust selector if needed, or re-run translation for it
621
+ const appLoadingBody = mapDiv.querySelector(
622
+ '[data-i18n="html.app_loading_body"], [din="html.app_loading_body"]'
623
+ );
624
+ if (appLoadingBody) {
625
+ (appLoadingBody as HTMLElement).innerHTML = imageExtractor(
626
+ ui.t("html.app_loading_body")
627
+ );
628
+ }
629
+
630
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
631
+ const options: any = {
632
+ reverse: true,
633
+ tipLabel: ui.t("control.trans", { ns: "translation" })
634
+ };
635
+ if (restoreTransparency) {
636
+ options.initialValue = restoreTransparency / 100;
637
+ }
638
+ ui.sliderNew = new SliderNew(options);
639
+ ui.core!.appData!.controls = [
640
+ new Copyright({
641
+ tipLabel: ui.t("control.info", { ns: "translation" })
642
+ }),
643
+ new CompassRotate({
644
+ tipLabel: ui.t("control.compass", { ns: "translation" })
645
+ }),
646
+ new Zoom({
647
+ tipLabel: ui.t("control.zoom", { ns: "translation" })
648
+ }),
649
+ new SetGPS({
650
+ ui,
651
+ tipLabel: ui.t("control.gps", { ns: "translation" })
652
+ }),
653
+ new GoHome({
654
+ tipLabel: ui.t("control.home", { ns: "translation" })
655
+ }),
656
+ ui.sliderNew,
657
+ new Maplat({
658
+ tipLabel: ui.t("control.help", { ns: "translation" })
659
+ })
660
+ ];
661
+ if (ui.enableShare) {
662
+ ui.core!.appData!.controls.push(
663
+ new Share({
664
+ tipLabel: ui.t("control.share", { ns: "translation" })
665
+ })
666
+ );
667
+ }
668
+ if (ui.enableBorder) {
669
+ ui.core!.appData!.controls.push(
670
+ new Border({
671
+ tipLabel: ui.t("control.border", { ns: "translation" })
672
+ })
673
+ );
674
+ }
675
+ if (ui.enableHideMarker) {
676
+ ui.core!.appData!.controls.push(
677
+ new HideMarker({
678
+ tipLabel: ui.t("control.hide_marker", { ns: "translation" })
679
+ })
680
+ );
681
+ }
682
+ if (ui.enableMarkerList) {
683
+ ui.core!.appData!.controls.push(
684
+ new MarkerList({
685
+ tipLabel: ui.t("control.marker_list", { ns: "translation" })
686
+ })
687
+ );
688
+ }
689
+
690
+ // Contextmenu
691
+ ui.contextMenu = new ContextMenu({
692
+ eventType: "__dummy__",
693
+ width: 170,
694
+ defaultItems: false,
695
+ items: []
696
+ });
697
+ ui.core!.appData!.controls.push(ui.contextMenu);
698
+
699
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
700
+ ui.sliderNew.on("propertychange", (evt: any) => {
701
+ if (evt.key === "slidervalue") {
702
+ ui.core!.setTransparency(ui.sliderNew.get(evt.key) * 100);
703
+ ui.updateUrl();
704
+ }
705
+ });
706
+
707
+ if (enableSplash) {
708
+ // Check Splash data
709
+ let splash = false;
710
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
711
+ if ((ui.core!.appData as any).splash) splash = true;
712
+
713
+ // const modal = new bsn.Modal(modalElm, { root: mapDiv });
714
+ const modal = prepareModal(modalBase, { root: mapDiv });
715
+
716
+ (mapDiv.querySelector(".modal_load_title") as HTMLElement).innerText =
717
+ ui.translate(ui.core!.appData!.appName) || "";
718
+ if (splash) {
719
+ mapDiv
720
+ .querySelector(".splash_img")!
721
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
722
+ .setAttribute("src", `img/${(ui.core!.appData as any).splash}`);
723
+ mapDiv.querySelector(".splash_div")!.classList.remove("hide");
724
+ }
725
+ ui.modalSetting("load");
726
+ modal.show();
727
+
728
+ const fadeTime = splash ? 1000 : 200;
729
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
730
+ ui.splashPromise = new Promise((resolve: any) => {
731
+ setTimeout(() => {
732
+ resolve();
733
+ }, fadeTime);
734
+ });
735
+ }
736
+
737
+ document.querySelector("title")!.innerHTML =
738
+ ui.translate(ui.core!.appName) || "";
739
+ });
740
+
741
+ ui.core!.waitReady.then(() => {
742
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
743
+ ui.core!.mapObject.on("click_control", (evt: any) => {
744
+ const control = evt.control || (evt.frameState && evt.frameState.control);
745
+ const modal = prepareModal(modalBase);
746
+
747
+ if (control === "help") {
748
+ ui.modalSetting("help");
749
+ modal.show();
750
+ } else if (control === "share") {
751
+ ui.modalSetting("share");
752
+
753
+ const modalBody = modalBase.querySelector(".modal-body") as HTMLElement;
754
+
755
+ const baseUrl = ui.getShareUrl("app");
756
+ const viewUrl = ui.getShareUrl("view");
757
+
758
+ // Generate QR Codes
759
+ const qrAppDiv = modalBody.querySelector(".qr_app") as HTMLElement;
760
+ const qrViewDiv = modalBody.querySelector(".qr_view") as HTMLElement;
761
+
762
+ if (qrAppDiv) {
763
+ QRCode.toCanvas(
764
+ baseUrl,
765
+ { width: 128, margin: 1 },
766
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
767
+ (err: any, canvas: any) => {
768
+ if (!err) {
769
+ qrAppDiv.innerHTML = "";
770
+ qrAppDiv.appendChild(canvas);
771
+ }
772
+ }
773
+ );
774
+ }
775
+ if (qrViewDiv) {
776
+ QRCode.toCanvas(
777
+ viewUrl,
778
+ { width: 128, margin: 1 },
779
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
780
+ (err: any, canvas: any) => {
781
+ if (!err) {
782
+ qrViewDiv.innerHTML = "";
783
+ qrViewDiv.appendChild(canvas);
784
+ }
785
+ }
786
+ );
787
+ }
788
+
789
+ modal.show();
790
+ } else if (control === "markerList") {
791
+ ui.modalSetting("marker_list");
792
+ modal.show();
793
+
794
+ const listRoot = modalBase.querySelector(
795
+ ".modal_marker_list_content ul.list-group"
796
+ ) as HTMLElement;
797
+
798
+ // Reset all panel states when modal is closed
799
+ const resetPanels = () => {
800
+ // Close all Layer panels
801
+ const allLayerPanels =
802
+ listRoot.querySelectorAll(".list_poiitems_div");
803
+ allLayerPanels.forEach(panel => {
804
+ panel.classList.remove("open");
805
+ });
806
+
807
+ // Close and reset all POI Content panels
808
+ const allPoiContentDivs = listRoot.querySelectorAll(
809
+ ".list_poicontent_div"
810
+ );
811
+ allPoiContentDivs.forEach(contentDiv => {
812
+ contentDiv.classList.remove("open");
813
+ contentDiv.innerHTML = "";
814
+ });
815
+
816
+ // Unselect marker
817
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
818
+ (ui.core as any).unselectMarker?.();
819
+
820
+ // Remove this event listener after execution
821
+ modalBase.removeEventListener("hidden.bs.modal", resetPanels);
822
+ };
823
+
824
+ // Remove old listener if exists and add new one
825
+ modalBase.removeEventListener("hidden.bs.modal", resetPanels);
826
+ modalBase.addEventListener("hidden.bs.modal", resetPanels);
827
+
828
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
829
+ const currentMapID = (ui.core!.from as any).mapID;
830
+ if (cachedMarkerListMapID === currentMapID) return;
831
+ cachedMarkerListMapID = currentMapID;
832
+
833
+ listRoot.innerHTML = "";
834
+
835
+ const layers = ui.core!.listPoiLayers(false, true);
836
+
837
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
838
+ layers.forEach((layer: any) => {
839
+ // Create Layer Item
840
+ const layerLi = createElement(`<li class="list-group-item layer">
841
+ <div class="row layer_row">
842
+ <div class="layer_label">
843
+ <span class="dli-chevron"></span>
844
+ <img src="${layer.icon || pointer["defaultpin.png"]}" class="markerlist"> ${ui.translate!(layer.name)}
845
+ </div>
846
+ <div class="layer_onoff">
847
+ <input type="checkbox" class="markerlist" ${layer.hide ? "" : "checked"}>
848
+ <label class="check"><div></div></label>
849
+ </div>
850
+ </div>
851
+ </li>`)[0] as HTMLElement;
852
+
853
+ const checkbox = layerLi.querySelector(
854
+ "input[type=checkbox]"
855
+ ) as HTMLInputElement;
856
+ const label = layerLi.querySelector("label.check") as HTMLElement;
857
+
858
+ label.addEventListener("click", e => {
859
+ e.stopPropagation();
860
+ if (!checkbox.disabled) {
861
+ checkbox.checked = !checkbox.checked;
862
+ checkbox.dispatchEvent(new Event("change"));
863
+ }
864
+ });
865
+
866
+ checkbox.addEventListener("click", e => e.stopPropagation());
867
+
868
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
869
+ checkbox.addEventListener("change", (e: any) => {
870
+ if (e.target.checked) {
871
+ ui.core!.showPoiLayer(layer.id);
872
+ } else {
873
+ ui.core!.hidePoiLayer(layer.id);
874
+ }
875
+ });
876
+
877
+ const poiListUl = createElement(
878
+ `<ul class="list_poiitems_div"></ul>`
879
+ )[0] as HTMLElement;
880
+
881
+ layerLi
882
+ .querySelector(".layer_label")!
883
+ .addEventListener("click", () => {
884
+ const isCurrentlyOpen = poiListUl.classList.contains("open");
885
+
886
+ // Close all other Layer panels at the same level
887
+ const allLayerPanels =
888
+ listRoot.querySelectorAll(".list_poiitems_div");
889
+ allLayerPanels.forEach(panel => {
890
+ if (panel !== poiListUl) {
891
+ panel.classList.remove("open");
892
+ // Reset all child POI Content panels
893
+ const poiContentDivs = panel.parentElement!.querySelectorAll(
894
+ ".list_poicontent_div"
895
+ );
896
+ poiContentDivs.forEach(contentDiv => {
897
+ contentDiv.classList.remove("open");
898
+ contentDiv.innerHTML = "";
899
+ });
900
+ }
901
+ });
902
+
903
+ // Toggle current panel
904
+ if (isCurrentlyOpen) {
905
+ poiListUl.classList.remove("open");
906
+ // Reset all child POI Content panels in this Layer
907
+ const poiContentDivs = poiListUl.querySelectorAll(
908
+ ".list_poicontent_div"
909
+ );
910
+ poiContentDivs.forEach(contentDiv => {
911
+ contentDiv.classList.remove("open");
912
+ contentDiv.innerHTML = "";
913
+ });
914
+ (ui.core as MaplatApp).unselectMarker?.();
915
+ } else {
916
+ poiListUl.classList.add("open");
917
+ }
918
+ });
919
+
920
+ listRoot.appendChild(layerLi);
921
+ listRoot.appendChild(poiListUl);
922
+
923
+ if (layer.pois) {
924
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
925
+ layer.pois.forEach((poi: any) => {
926
+ const poiLi = createElement(`<li class="list-group-item poi">
927
+ <div class="row poi_row">
928
+ <div class="poi_label">
929
+ <span class="dli-chevron"></span>
930
+ <img src="${poi.icon || layer.icon || pointer["defaultpin.png"]}" class="markerlist"> ${ui.translate!(poi.name)}
931
+ </div>
932
+ </div>
933
+ </li>`)[0] as HTMLElement;
934
+
935
+ const poiContentDiv = createElement(
936
+ `<div class="list_poicontent_div"></div>`
937
+ )[0] as HTMLElement;
938
+
939
+ // let poiImgHide: any;
940
+
941
+ poiLi.addEventListener("click", () => {
942
+ const isCurrentlyOpen =
943
+ poiContentDiv.classList.contains("open");
944
+
945
+ // Close all other POI panels at the same level (within the same Layer)
946
+ const allPoiContentDivs = poiListUl.querySelectorAll(
947
+ ".list_poicontent_div"
948
+ );
949
+ allPoiContentDivs.forEach(contentDiv => {
950
+ if (
951
+ contentDiv !== poiContentDiv &&
952
+ contentDiv.classList.contains("open")
953
+ ) {
954
+ contentDiv.classList.remove("open");
955
+ contentDiv.innerHTML = "";
956
+ }
957
+ });
958
+
959
+ // Toggle current panel
960
+ if (isCurrentlyOpen) {
961
+ poiContentDiv.classList.remove("open");
962
+ poiContentDiv.innerHTML = "";
963
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
964
+ (ui.core as any).unselectMarker?.();
965
+ } else {
966
+ poiContentDiv.classList.add("open");
967
+ poiWebControl(ui, poiContentDiv, poi, false);
968
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
969
+ (ui.core as any).selectMarker?.(poi.namespaceID);
970
+ }
971
+ });
972
+
973
+ poiListUl.appendChild(poiLi);
974
+ poiListUl.appendChild(poiContentDiv);
975
+ });
976
+ }
977
+ });
978
+ } else if (control === "copyright") {
979
+ ui.modalSetting("map");
980
+ const mapData = ui.core!.from!;
981
+ const titleEl = mapDiv.querySelector(".modal_map .modal_title");
982
+ if (titleEl) {
983
+ const titleVal = mapData.get ? mapData.get("title") : mapData.title;
984
+ (titleEl as HTMLElement).innerText = ui.translate!(titleVal) || "";
985
+ }
986
+
987
+ META_KEYS.forEach(key => {
988
+ if (key === "title" || key === "officialTitle") return;
989
+ const val = mapData.get
990
+ ? mapData.get(key)
991
+ : // eslint-disable-next-line @typescript-eslint/no-explicit-any
992
+ (mapData as any)[key];
993
+ const container = mapDiv.querySelector(`.modal_map .${key}_div`);
994
+ if (container) {
995
+ if (val) {
996
+ (container as HTMLElement).style.display = "block";
997
+ const contentEl = container.querySelector(`.${key}_dd`);
998
+ if (contentEl) {
999
+ if (key === "license" || key === "dataLicense") {
1000
+ const fileName = (val as string)
1001
+ .toLowerCase()
1002
+ .replace(/ /g, "_");
1003
+
1004
+ const iconUrl =
1005
+ pointer[`${fileName}.png`] ||
1006
+ `assets/parts/${fileName}.png`;
1007
+ (contentEl as HTMLElement).innerHTML =
1008
+ `<img src="${iconUrl}" class="license" />`;
1009
+ } else {
1010
+ (contentEl as HTMLElement).innerHTML =
1011
+ ui.translate!(val) || "";
1012
+ }
1013
+ }
1014
+ } else {
1015
+ (container as HTMLElement).style.display = "none";
1016
+ }
1017
+ }
1018
+ });
1019
+
1020
+ const cacheDiv = mapDiv.querySelector(
1021
+ ".modal_cache_content"
1022
+ ) as HTMLElement;
1023
+ const cacheSize = cacheDiv.querySelector(".cache_size") as HTMLElement;
1024
+ let cacheFetch = cacheDiv.querySelector(
1025
+ ".cache_fetch"
1026
+ ) as HTMLButtonElement;
1027
+ let cacheDelete = cacheDiv.querySelector(
1028
+ ".cache_delete"
1029
+ ) as HTMLButtonElement;
1030
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1031
+ const weiwudi = (mapData as any).weiwudi;
1032
+ if (
1033
+ ui.core!.enableCache &&
1034
+ weiwudi &&
1035
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1036
+ !(mapData as any).vector
1037
+ ) {
1038
+ cacheDiv.style.display = "block";
1039
+ cacheFetch.style.display = "none";
1040
+ cacheDelete.style.display = "none";
1041
+ const totalTile = weiwudi.totalTile;
1042
+ let isFetching = false;
1043
+
1044
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1045
+ let currentStats: any = undefined;
1046
+
1047
+ const updateButtons = () => {
1048
+ const t = ui.t ? ui.t.bind(ui) : ui.translate!.bind(ui);
1049
+
1050
+ if (totalTile) {
1051
+ cacheFetch.style.display = "inline-block";
1052
+ if (isFetching) {
1053
+ cacheFetch.innerHTML =
1054
+ t("html.cache_cancel") || "Cancel download";
1055
+ cacheFetch.classList.remove("btn-default");
1056
+ cacheFetch.classList.add("btn-danger");
1057
+ if (!cacheFetch.classList.contains("btn-default"))
1058
+ cacheFetch.classList.add("btn-default");
1059
+ cacheFetch.disabled = false;
1060
+ } else {
1061
+ cacheFetch.innerHTML = t("html.cache_fetch") || "Bulk download";
1062
+ if (!cacheFetch.classList.contains("btn-default"))
1063
+ cacheFetch.classList.add("btn-default");
1064
+ cacheFetch.classList.remove("btn-danger");
1065
+
1066
+ // Disable if 100%
1067
+ if (currentStats && currentStats.count === currentStats.total) {
1068
+ cacheFetch.disabled = true;
1069
+ // User requested disabled, not hidden
1070
+ cacheFetch.style.display = "inline-block";
1071
+ } else {
1072
+ cacheFetch.disabled = false;
1073
+ cacheFetch.style.display = "inline-block";
1074
+ }
1075
+ }
1076
+ } else {
1077
+ cacheFetch.style.display = "none";
1078
+ }
1079
+
1080
+ if (currentStats && currentStats.size > 0) {
1081
+ cacheDelete.style.display = "inline-block";
1082
+ // Disable if fetching
1083
+ cacheDelete.disabled = isFetching;
1084
+ } else {
1085
+ // User requested disabled, not hidden
1086
+ cacheDelete.style.display = "inline-block";
1087
+ cacheDelete.disabled = true;
1088
+ }
1089
+ if (isFetching) cacheDelete.disabled = true; // Double ensure
1090
+ };
1091
+
1092
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1093
+ const showStats = async (stats: any | undefined = undefined) => {
1094
+ if (!stats) stats = await weiwudi.stats();
1095
+ currentStats = stats;
1096
+
1097
+ const t = ui.t ? ui.t.bind(ui) : ui.translate!.bind(ui);
1098
+
1099
+ const sizeStr = isFetching
1100
+ ? t("html.cache_processing") || "Calculating..."
1101
+ : encBytes(stats.size || 0);
1102
+
1103
+ if (totalTile) {
1104
+ const count = stats.count || 0;
1105
+ const percent = Math.floor((1000 * count) / totalTile);
1106
+ cacheSize.innerText = `${sizeStr} (${
1107
+ count
1108
+ } / ${totalTile} tiles [${percent / 10}%])`;
1109
+ } else {
1110
+ cacheSize.innerText = `${sizeStr} (${stats.count || 0} tiles)`;
1111
+ }
1112
+ updateButtons();
1113
+ };
1114
+ showStats();
1115
+
1116
+ let updateFrame = 0;
1117
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1118
+ let latestStats: any = null;
1119
+
1120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1121
+ const fetchHandler = (evt: any) => {
1122
+ if (evt.type === "proceed") {
1123
+ isFetching = true;
1124
+ latestStats = {
1125
+ size: evt.detail.size || currentStats?.size || 0,
1126
+ count: evt.detail.processed || evt.detail.count || 0,
1127
+ total: evt.detail.total || totalTile || 0
1128
+ };
1129
+
1130
+ if (!updateFrame) {
1131
+ updateFrame = requestAnimationFrame(() => {
1132
+ updateFrame = 0;
1133
+ try {
1134
+ if (latestStats) showStats(latestStats);
1135
+ } catch (e) {
1136
+ console.error("Error in showStats:", e);
1137
+ }
1138
+ });
1139
+ }
1140
+ } else if (
1141
+ evt.type === "finish" ||
1142
+ evt.type === "stop" ||
1143
+ evt.type === "canceled"
1144
+ ) {
1145
+ if (updateFrame) {
1146
+ cancelAnimationFrame(updateFrame);
1147
+ updateFrame = 0;
1148
+ }
1149
+ isFetching = false;
1150
+ latestStats = null;
1151
+ showStats();
1152
+ }
1153
+ };
1154
+ // Weiwudi might dispatch 'proceed', 'finish', 'stop', 'canceled'
1155
+ weiwudi.addEventListener("proceed", fetchHandler);
1156
+ weiwudi.addEventListener("finish", fetchHandler);
1157
+ weiwudi.addEventListener("stop", fetchHandler);
1158
+ // Check if weiwudi dispatches 'canceled'. Based on my read, it does dispatch e.data.type
1159
+ // And weiwudi_gw_logic sends type: 'canceled'.
1160
+ weiwudi.addEventListener("canceled", fetchHandler);
1161
+
1162
+ const removeListeners = () => {
1163
+ weiwudi.removeEventListener("proceed", fetchHandler);
1164
+ weiwudi.removeEventListener("finish", fetchHandler);
1165
+ weiwudi.removeEventListener("stop", fetchHandler);
1166
+ weiwudi.removeEventListener("canceled", fetchHandler);
1167
+ modalBase.removeEventListener("hidden.bs.modal", removeListeners);
1168
+ };
1169
+ modalBase.addEventListener("hidden.bs.modal", removeListeners);
1170
+
1171
+ const newElem = cacheFetch.cloneNode(true);
1172
+ cacheFetch.parentNode!.replaceChild(newElem, cacheFetch);
1173
+ // Initial update handled by showStats -> updateButtons
1174
+ cacheFetch = newElem as HTMLButtonElement;
1175
+
1176
+ cacheFetch.addEventListener("click", async () => {
1177
+ if (isFetching) {
1178
+ await weiwudi.cancel();
1179
+ } else {
1180
+ isFetching = true; // Set immediately to update UI
1181
+ updateButtons();
1182
+ try {
1183
+ await weiwudi.fetchAll();
1184
+ } catch {
1185
+ isFetching = false;
1186
+ updateButtons();
1187
+ }
1188
+ }
1189
+ await showStats();
1190
+ });
1191
+
1192
+ const newElem2 = cacheDelete.cloneNode(true);
1193
+ cacheDelete.parentNode!.replaceChild(newElem2, cacheDelete);
1194
+ cacheDelete = newElem2 as HTMLButtonElement;
1195
+ cacheDelete.innerHTML = ui.t("html.cache_delete");
1196
+
1197
+ cacheDelete.addEventListener("click", async () => {
1198
+ if (isFetching) return; // Should be disabled, but safety check
1199
+ await weiwudi.clean();
1200
+ await showStats();
1201
+ });
1202
+ } else {
1203
+ cacheDiv.style.display = "none";
1204
+ }
1205
+
1206
+ modal.show();
1207
+ } else if (control === "border") {
1208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1209
+ ui.setShowBorder(!(ui.core!.stateBuffer as any).showBorder);
1210
+ } else if (control === "hideMarker") {
1211
+ const current = mapDiv.classList.contains("hide-marker");
1212
+ ui.setHideMarker(!current);
1213
+ }
1214
+ ui.updateUrl();
1215
+ });
1216
+ });
1217
+ }
1218
+
1219
+ function initDom(ui: MaplatUi, appOption: MaplatAppOption) {
1220
+ // Inject Custom Toast Styles
1221
+ const style = document.createElement("style");
1222
+ style.innerHTML = `
1223
+ .custom-toast {
1224
+ visibility: hidden;
1225
+ min-width: 250px;
1226
+ margin-left: -125px;
1227
+ background-color: #333;
1228
+ color: #fff;
1229
+ text-align: center;
1230
+ border-radius: 2px;
1231
+ padding: 16px;
1232
+ position: fixed;
1233
+ z-index: 9999;
1234
+ left: 50%;
1235
+ bottom: 30px;
1236
+ font-size: 17px;
1237
+ opacity: 0;
1238
+ transition: opacity 0.3s;
1239
+ }
1240
+ .custom-toast.show {
1241
+ visibility: visible;
1242
+ opacity: 1;
1243
+ }
1244
+ `;
1245
+ document.head.appendChild(style);
1246
+
1247
+ let pwaManifest = appOption.pwaManifest;
1248
+ let pwaWorker = appOption.pwaWorker;
1249
+ let pwaScope = appOption.pwaScope;
1250
+
1251
+ // Add UI HTML Element
1252
+ let newElems = createElement(`<d c="ol-control map-title"><s></s></d>
1253
+ <d c="swiper-container ol-control base-swiper prevent-default-ui">
1254
+ <d c="swiper-wrapper"> </d>
1255
+ <d c="swiper-button-next base-next swiper-button-white"> </d>
1256
+ <d c="swiper-button-prev base-prev swiper-button-white"> </d>
1257
+ </d>
1258
+ <d c="swiper-container ol-control overlay-swiper prevent-default-ui">
1259
+ <d c="swiper-wrapper"> </d>
1260
+ <d c="swiper-button-next overlay-next swiper-button-white"> </d>
1261
+ <d c="swiper-button-prev overlay-prev swiper-button-white"> </d>
1262
+ </d> `);
1263
+ for (let i = newElems.length - 1; i >= 0; i--) {
1264
+ ui.core!.mapDivDocument!.insertBefore(
1265
+ newElems[i],
1266
+ ui.core!.mapDivDocument!.firstChild
1267
+ );
1268
+ }
1269
+
1270
+ newElems = createElement(`<d c="modal modalBase" tabindex="-1" role="dialog"
1271
+ aria-labelledby="staticModalLabel" aria-hidden="true" data-show="true" data-keyboard="false"
1272
+ data-backdrop="static">
1273
+ <d c="modal-dialog">
1274
+ <d c="modal-content">
1275
+ <d c="modal-header">
1276
+ <button type="button" c="close" data-dismiss="modal">
1277
+ <s aria-hidden="true">&#215;</s><s c="sr-only" din="html.close"></s>
1278
+ </button>
1279
+ <h4 c="modal-title">
1280
+
1281
+ <s c="modal_title"></s>
1282
+ <s c="modal_load_title"></s>
1283
+ <s c="modal_gpsW_title" din="html.acquiring_gps"></s>
1284
+ <s c="modal_help_title" din="html.help_title"></s>
1285
+ <s c="modal_share_title" din="html.share_title"></s>
1286
+ <s c="modal_marker_list_title" din="html.marker_list_title"></s>
1287
+
1288
+ </h4>
1289
+ </d>
1290
+ <d c="modal-body">
1291
+
1292
+ <d c="modal_help_content">
1293
+ <d c="help_content">
1294
+ <s dinh="html.help_using_maplat"></s>
1295
+ <p c="col-xs-12 help_img"><img src="${pointer["fullscreen.png"]}"></p>
1296
+ <h4 din="html.help_operation_title"></h4>
1297
+ <p dinh="html.help_operation_content" c="recipient"></p>
1298
+ <h4 din="html.help_selection_title"></h4>
1299
+ <p dinh="html.help_selection_content" c="recipient"></p>
1300
+ <h4 din="html.help_gps_title"></h4>
1301
+ <p dinh="html.help_gps_content" c="recipient"></p>
1302
+ <h4 din="html.help_poi_title"></h4>
1303
+ <p dinh="html.help_poi_content" c="recipient"></p>
1304
+ <h4 din="html.help_etc_title"></h4>
1305
+ <ul>
1306
+ <li dinh="html.help_etc_attr" c="recipient"></li>
1307
+ <li dinh="html.help_etc_help" c="recipient"></li>
1308
+ <s c="share_help"><li dinh="html.help_share_help" c="recipient"></li></s>
1309
+ <s c="border_help"><li dinh="html.help_etc_border" c="recipient"></li></s>
1310
+ <s c="hide_marker_help"><li dinh="html.help_etc_hide_marker" c="recipient"></li></s>
1311
+ <s c="marker_list_help"><li dinh="html.help_etc_marker_list" c="recipient"></li></s>
1312
+ <li dinh="html.help_etc_slider" c="recipient"></li>
1313
+ </ul>
1314
+ <p><a href="https://www.maplat.jp/" target="_blank">Maplat</a>
1315
+ © 2015- Kohei Otsuka, Code for History</p>
1316
+ </d>
1317
+ </d>
1318
+
1319
+ <d c="modal_poi_content">
1320
+ <d c="poi_web_div"></d>
1321
+ <d c="poi_share_buttons recipient row">
1322
+ <d c="form-group col-xs-4 text-center"><button title="Copy to clipboard" class="share btn btn-light" data="cp_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M224 0c-35.3 0-64 28.7-64 64V96H96c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H288c35.3 0 64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H224zM288 384H96V160H224c0-17.7 14.3-32 32-32h64V256c0 17.7 14.3 32 32 32h96V384H288z"/></svg>&nbsp;<small din="html.share_copy"></small></button></d>
1323
+ <d c="form-group col-xs-4 text-center"><button title="Twitter" class="share btn btn-light" data="tw_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>&nbsp;<small>Twitter</small></button></d>
1324
+ <d c="form-group col-xs-4 text-center"><button title="Facebook" class="share btn btn-light" data="fb_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256C504 119 393 8 256 8S8 119 8 256c0 121.3 87.1 222.4 203 240.5V327.9h-61v-71.9h61V203c0-60.8 35.8-93.7 89.2-93.7 25.5 0 50.4 1.8 56.1 2.6v62.4h-35.4c-29.5 0-37.4 18.2-37.4 42.1v59.6h68.9l-11 71.9h-57.9V496.5C416.9 478.4 504 377.3 504 256z"/></svg>&nbsp;<small>Facebook</small></button></d>
1325
+ </d>
1326
+ <d c="qr_view_poi center-block" style="width:128px;"></d>
1327
+ <d c="modal_share_poi"></d>
1328
+ <p><img src="" height="0px" width="0px"></p>
1329
+ </d>
1330
+
1331
+ <d c="modal_share_content">
1332
+ <h4 din="html.share_app_title"></h4>
1333
+ <d id="___maplat_app_toast_${ui.html_id_seed}"></d>
1334
+ <d c="recipient row">
1335
+ <d c="form-group col-xs-4 text-center"><button title="Copy to clipboard" class="share btn btn-light" data="cp_app"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M224 0c-35.3 0-64 28.7-64 64V96H96c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H288c35.3 0 64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H224zM288 384H96V160H224c0-17.7 14.3-32 32-32h64V256c0 17.7 14.3 32 32 32h96V384H288z"/></svg>&nbsp;<small din="html.share_copy"></small></button></d>
1336
+ <d c="form-group col-xs-4 text-center"><button title="Twitter" class="share btn btn-light" data="tw_app"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>&nbsp;<small>Twitter</small></button></d>
1337
+ <d c="form-group col-xs-4 text-center"><button title="Facebook" class="share btn btn-light" data="fb_app"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256C504 119 393 8 256 8S8 119 8 256c0 121.3 87.1 222.4 203 240.5V327.9h-61v-71.9h61V203c0-60.8 35.8-93.7 89.2-93.7 25.5 0 50.4 1.8 56.1 2.6v62.4h-35.4c-29.5 0-37.4 18.2-37.4 42.1v59.6h68.9l-11 71.9h-57.9V496.5C416.9 478.4 504 377.3 504 256z"/></svg>&nbsp;<small>Facebook</small></button></d>
1338
+ </d>
1339
+ <d c="qr_app center-block" style="width:128px;"></d>
1340
+ <d c="modal_share_state">
1341
+ <h4 din="html.share_state_title"></h4>
1342
+ <d id="___maplat_view_toast_${ui.html_id_seed}"></d>
1343
+ <d c="recipient row">
1344
+ <d c="form-group col-xs-4 text-center"><button title="Copy to clipboard" c="share btn btn-light" data="cp_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M224 0c-35.3 0-64 28.7-64 64V96H96c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H288c35.3 0 64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H224zM288 384H96V160H224c0-17.7 14.3-32 32-32h64V256c0 17.7 14.3 32 32 32h96V384H288z"/></svg>&nbsp;<small din="html.share_copy"></small></button></d>
1345
+ <d c="form-group col-xs-4 text-center"><button title="Twitter" c="share btn btn-light" data="tw_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>&nbsp;<small>Twitter</small></button></d>
1346
+ <d c="form-group col-xs-4 text-center"><button title="Facebook" c="share btn btn-light" data="fb_view"><svg style="width:14px;height:14px;vertical-align:text-bottom;" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256C504 119 393 8 256 8S8 119 8 256c0 121.3 87.1 222.4 203 240.5V327.9h-61v-71.9h61V203c0-60.8 35.8-93.7 89.2-93.7 25.5 0 50.4 1.8 56.1 2.6v62.4h-35.4c-29.5 0-37.4 18.2-37.4 42.1v59.6h68.9l-11 71.9h-57.9V496.5C416.9 478.4 504 377.3 504 256z"/></svg>&nbsp;<small>Facebook</small></button></d>
1347
+ </d>
1348
+ <d c="qr_view center-block" style="width:128px;"></d>
1349
+ </d>
1350
+ <p><img src="" height="0px" width="0px"></p>
1351
+ </d>
1352
+
1353
+ <d c="modal_map_content">
1354
+ ${META_KEYS.map(key => {
1355
+ if (key == "title" || key == "officialTitle") return "";
1356
+ return `<d c="recipients ${key}_div"><dl c="dl-horizontal">
1357
+ <dt din="html.${key}"></dt>
1358
+ <dd c="${key}_dd"></dd>
1359
+ </dl></d> `;
1360
+ }).join("")}
1361
+ <d c="recipients modal_cache_content"><dl c="dl-horizontal">
1362
+ <dt din="html.cache_handle"></dt>
1363
+ <dd><s c="cache_size"></s></dd>
1364
+ <dt></dt>
1365
+ <dd><s c="pull-right"><button c="cache_fetch btn btn-default" href="#" din="html.cache_fetch"></button>
1366
+ <button c="cache_delete btn btn-default" href="#" din="html.cache_delete"></button></s></dd>
1367
+ </dl></d>
1368
+ </d>
1369
+
1370
+ <d c="modal_load_content">
1371
+ <p c="recipient"><img src="${pointer["loading.png"]}"><s din="html.app_loading_body"></s></p>
1372
+ <d c="splash_div hide row"><p c="col-xs-12 poi_img"><img c="splash_img" src=""></p></d>
1373
+ <p><img src="" height="0px" width="0px"></p>
1374
+ </d>
1375
+
1376
+ <d c="modal_marker_list_content">
1377
+ <ul c="list-group"></ul>
1378
+ </d>
1379
+
1380
+ <p c="modal_gpsD_content" c="recipient"></p>
1381
+ <p c="modal_gpsW_content" c="recipient"></p>
1382
+
1383
+ </d>
1384
+ </d>
1385
+ </d>
1386
+ </d> `);
1387
+
1388
+ for (let i = newElems.length - 1; i >= 0; i--) {
1389
+ ui.core!.mapDivDocument!.insertBefore(
1390
+ newElems[i],
1391
+ ui.core!.mapDivDocument!.firstChild
1392
+ );
1393
+ }
1394
+
1395
+ // PWA
1396
+ if (pwaManifest) {
1397
+ if (pwaManifest === true) {
1398
+ pwaManifest = `./pwa/${ui.core!.appid}_manifest.json`;
1399
+ }
1400
+ if (!pwaWorker) {
1401
+ pwaWorker = "./service-worker.js";
1402
+ }
1403
+ if (!pwaScope) {
1404
+ pwaScope = "./";
1405
+ }
1406
+
1407
+ const head = document.querySelector("head");
1408
+ if (head) {
1409
+ if (!head.querySelector('link[rel="manifest"]')) {
1410
+ head.appendChild(
1411
+ createElement(`<link rel="manifest" href="${pwaManifest}">`)[0]
1412
+ );
1413
+ }
1414
+ }
1415
+ try {
1416
+ Weiwudi.registerSW(pwaWorker, { scope: pwaScope });
1417
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1418
+ } catch (_e) {} // eslint-disable-line no-empty
1419
+
1420
+ if (head && !head.querySelector('link[rel="apple-touch-icon"]')) {
1421
+ fetch(pwaManifest)
1422
+ .then(response => response.json())
1423
+ .then(value => {
1424
+ if (value.icons) {
1425
+ value.icons.forEach((icon: { src: string; sizes: string }) => {
1426
+ const src = absoluteUrl(pwaManifest as string, icon.src);
1427
+ const sizes = icon.sizes;
1428
+ const tag = `<link rel="apple-touch-icon" sizes="${sizes}" href="${src}">`;
1429
+ head.appendChild(createElement(tag)[0]);
1430
+ });
1431
+ }
1432
+ })
1433
+ .catch(err => {
1434
+ console.error("Failed to fetch PWA manifest:", err);
1435
+ });
1436
+ }
1437
+ }
1438
+ }