@maplat/ui 0.11.10 → 0.12.0

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