splatone 0.0.1

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.
@@ -0,0 +1,740 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <title>
7
+ <%= title %>
8
+ </title>
9
+
10
+ <!-- Leaflet CSS -->
11
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="anonymous" />
12
+ <!-- Leaflet.draw CSS -->
13
+ <link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" crossorigin="anonymous" />
14
+
15
+ <link rel="stylesheet" href="/style.css" />
16
+ </head>
17
+
18
+ <body>
19
+ <!-- ハンバーガー -->
20
+ <button id="hamburger" class="hamburger" aria-label="Open controls" aria-expanded="false" aria-controls="sidepanel">
21
+
22
+ </button>
23
+
24
+ <!-- スライドオーバー(右) -->
25
+ <aside id="sidepanel" class="sidepanel" aria-hidden="true">
26
+ <header class="panel-header">
27
+ <h2>Controls</h2>
28
+ <button id="panelClose" class="icon-btn" aria-label="Close controls">✕</button>
29
+ </header>
30
+
31
+ <div class="panel-body">
32
+ <section class="panel-section">
33
+ <p class="muted">
34
+ 地図上で <strong>矩形をドラッグ</strong> して BBOX を指定してください。<br>
35
+ クリックした三角形と、<em>共有辺を持つ別Hexの三角形</em>が同時にハイライトされます。
36
+ </p>
37
+ </section>
38
+
39
+ <section class="panel-section">
40
+ <label class="field">
41
+ <span>Keywords</span>
42
+ <input id="keywords" type="text" value="<%= defaultKeywords %>" />
43
+ </label>
44
+ <label class="field">
45
+ <span>cellSize</span>
46
+ <input id="cellSize" type="number" step="0.1" min="0.05" value="<%= defaultCellSize %>" />
47
+ </label>
48
+
49
+ <label class="field">
50
+ <span>units</span>
51
+ <select id="units">
52
+ <option value="kilometers" <%=defaultUnits==='kilometers' ?'selected':'' %>>kilometers</option>
53
+ <option value="meters" <%=defaultUnits==='meters' ?'selected':'' %>>meters</option>
54
+ <option value="miles" <%=defaultUnits==='miles' ?'selected':'' %>>miles</option>
55
+ </select>
56
+ </label>
57
+
58
+ <label class="check">
59
+ <input id="toggleTriangles" type="checkbox" checked />
60
+ <span>Show triangles</span>
61
+ </label>
62
+
63
+ <label class="check">
64
+ <input id="toggleHexLabels" type="checkbox" />
65
+ <span>Show Hex IDs</span>
66
+ </label>
67
+
68
+ <div class="panel-actions">
69
+ <button id="clear" class="btn">Clear</button>
70
+ </div>
71
+
72
+ <div class="bbox-row">
73
+ <span id="bboxLabel" class="bbox-label"></span>
74
+ </div>
75
+ </section>
76
+
77
+ <section class="panel-section">
78
+ <details>
79
+ <summary>ヘルプ</summary>
80
+ <ul class="muted">
81
+ <li>左上の四角いツールで矩形を描画します。</li>
82
+ <li>矩形を編集すると自動で再生成します。</li>
83
+ <li>三角形をクリックすると、隣接する他Hexの三角形を同時ハイライト。</li>
84
+ </ul>
85
+ </details>
86
+ </section>
87
+ </div>
88
+ </aside>
89
+
90
+ <!-- パネルの背面オーバーレイ -->
91
+ <div id="panelOverlay" class="panel-overlay" hidden></div>
92
+
93
+ <!-- 地図 -->
94
+ <div id="map"></div>
95
+
96
+ <!-- Leaflet JS -->
97
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin="anonymous"></script>
98
+ <!-- Leaflet.draw JS -->
99
+ <script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js" crossorigin="anonymous"></script>
100
+
101
+ <!-- Socket.IO JS -->
102
+ <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
103
+ <!-- Visualize.js -->
104
+ <script src="visualizer.js"></script>
105
+ <!-- Main JS -->
106
+ <script type="module">
107
+ const lat = <%= lat %>;
108
+ const lon = <%= lon %>;
109
+ const socket = io({
110
+ path: "/socket",
111
+ reconnection: false
112
+ });
113
+
114
+ function setProgress(el, value) {
115
+ //プログレスバー
116
+ const v = Math.max(0, Math.min(100, Number(value)));
117
+ el.style.setProperty('--value', v);
118
+ el.setAttribute('aria-valuenow', String(v));
119
+ const label = el.querySelector('.progress__label');
120
+ if (label) label.textContent = v + '%';
121
+ }
122
+
123
+ function downloadJSON(filename, obj) {
124
+ const json = JSON.stringify(obj, null, 2);
125
+ const blob = new Blob([json], {
126
+ type: "application/json"
127
+ });
128
+ const url = URL.createObjectURL(blob);
129
+ const a = document.createElement("a");
130
+ a.href = url;
131
+ a.download = filename;
132
+ document.body.appendChild(a);
133
+ a.click();
134
+ a.remove();
135
+ URL.revokeObjectURL(url);
136
+ }
137
+
138
+ socket.on('connect', () => {
139
+ //console.log('Connected to server');
140
+ });
141
+
142
+ socket.on('disconnect', () => {
143
+ //console.log('Disconnected from server');
144
+ });
145
+
146
+ let sessionId=null;
147
+ let visualizers = {};
148
+ let layerControl=null;
149
+ socket.on("welcome",async (res) => {
150
+ //console.log(`welcome ${res.sessionId} at ${new Date(res.time).toLocaleTimeString()}`);
151
+ console.log("VIS",res.visualizers);
152
+ sessionId = res.sessionId;
153
+ visualizers = Object.fromEntries(await Promise.all(res.visualizers.map(async vis => { const m = await import(new URL(`./visualizer/${vis}/web.js`, import.meta.url).href); return [vis, m.entry ?? m.default ?? m[vis]]; })));
154
+ console.log(visualizers);
155
+ });
156
+
157
+ function generateLegend(legends) {
158
+ return legends.reduce(
159
+ (memo, legend) => {
160
+ return memo + `<li><span class="legend_circle" style="border-color:${legend.stroke};background:${legend.fill};"></span><span class="legend_category">${legend.category}</spam></li>`
161
+ },
162
+ "");
163
+ }
164
+ const BottomLeftProgressBar = L.Control.extend({
165
+ options: {
166
+ position: 'bottomleft'
167
+ },
168
+ onAdd(map) {
169
+ const div = L.DomUtil.create('div', 'map-bottom-left-progressbar');
170
+
171
+ div.innerHTML = `
172
+ <div class="panel-contents">
173
+ <!-- 幅は親 .progress-wrap の width/max-width で制御。 -->
174
+ <div id="progressCrawl" class="progress is-lg" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="32" aria-label="進捗" style="--value:32">
175
+ <div class="progress__bar" aria-hidden="true"></div>
176
+ <div class="progress__label" aria-hidden="true">32%</div>
177
+ </div>
178
+ </div>`;
179
+ // コントロール内の操作が地図ドラッグに奪われないようにする
180
+ L.DomEvent.disableClickPropagation(div);
181
+ L.DomEvent.disableScrollPropagation(div);
182
+ return div;
183
+ }
184
+ });
185
+ const BottomLeftLegend = L.Control.extend({
186
+ options: {
187
+ position: 'bottomleft'
188
+ },
189
+ onAdd(map) {
190
+ const div = L.DomUtil.create('div', 'map-bottom-left-legend');
191
+
192
+ div.innerHTML = `
193
+ <div class="panel-contents">
194
+ <div id="map_legend">
195
+ </div>
196
+ <div class="center-x">
197
+ <button id="download-json" class="dl-btn" title="Download JSON">
198
+ <!-- インラインSVG(ダウンロードアイコン) -->
199
+ <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
200
+ <path d="M12 3v9m0 0l4-4m-4 4L8 8m-5 9v2a2 2 0 002 2h14a2 2 0 002-2v-2"
201
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
202
+ </svg>
203
+ <span class="label">ダウンロード</span>
204
+ </button>
205
+ </div>
206
+ </div>`;
207
+ // コントロール内の操作が地図ドラッグに奪われないようにする
208
+ L.DomEvent.disableClickPropagation(div);
209
+ L.DomEvent.disableScrollPropagation(div);
210
+ return div;
211
+ }
212
+ });
213
+ socket.on('progress', (res) => {
214
+ //console.table(res.progress);
215
+ //console.log(Object.values(res.progress));
216
+ let {
217
+ crawled,
218
+ total
219
+ } = Object.values(res.progress).reduce(
220
+ (acc, g) => (g == null ? acc : {
221
+ crawled: acc.crawled + (g.crawled ?? 0),
222
+ total: acc.total + (g.total ?? 0)
223
+ }), {
224
+ crawled: 0,
225
+ total: 0
226
+ }
227
+ );
228
+ if (total == 0) {
229
+ crawled = 1, total = 1;
230
+ }
231
+ const percent = Math.round((crawled / total) * 100);
232
+ console.log(`[Done] ${crawled} / ${total} --> ${percent}`);
233
+ setProgress(document.getElementById("progressCrawl"), percent);
234
+ //ここでHexのプログレスグラデーションレイヤのOpacity調整
235
+ highlightHexById(res.hexId, 1 - res.progress[res.hexId].percent, "#263238");
236
+ });
237
+
238
+ function isPlainObject(a) {
239
+ if (a === null || typeof a !== 'object') return false; // 原始値/関数など除外
240
+ const proto = Object.getPrototypeOf(a);
241
+ return proto === Object.prototype || proto === null; // {} / Object.create(null)
242
+ }
243
+
244
+ let latestResult = {};
245
+ const overlays = {};
246
+ socket.on('result',async (res) => {
247
+ latestResult["_"]["visualizers"] = res.visualizers;
248
+ latestResult["_"]["plugin"] = res.plugin;
249
+ clearHexHighlight();
250
+ latestResult = {...latestResult, ...res.geoJson};
251
+ console.log("result");
252
+ console.log("geoJSON",res.geoJson);
253
+ for(const vis in visualizers){
254
+ const layers = await visualizers[vis](map, res.geoJson[vis], {palette:res.palette});
255
+ console.log("【レイヤ】",layers);
256
+ if((layers == null) ){
257
+ //SKIP
258
+ }else if(layers.hasOwnProperty("type") && layers["type"]=="FeatureCollection"){
259
+ //レイヤ一つ
260
+ overlays[`[${vis}]`] = layers;
261
+ }else{
262
+ for(const name in layers){
263
+ overlays[`[${vis}] ${name}`] = layers[name];
264
+ }
265
+ }
266
+ }
267
+ //進捗バーを消して凡例を表示
268
+ if (myProgress) map.removeControl(myProgress);
269
+ myLegend = new BottomLeftLegend().addTo(map);
270
+ document.getElementById('map_legend')?.replaceChildren();
271
+ const ul = document.createElement('div');
272
+ ul.className = 'legend_ul';
273
+ const palette = [];
274
+ for (const cat in res.palette) {
275
+ palette.push({
276
+ category: cat,
277
+ fill: res.palette[cat].color,
278
+ stroke: res.palette[cat].darken
279
+ });
280
+ }
281
+ ul.innerHTML = generateLegend(palette);
282
+ console.log(overlays);
283
+ layerControl = L.control.layers([baseLayer], {
284
+ ...overlays,
285
+ "Hex Grid": hexLayer,
286
+ "Tri Grid": triLayer
287
+ }, {
288
+ position: 'topleft'
289
+ });
290
+ layerControl.addTo(map);
291
+ document.getElementById("map_legend")?.appendChild(ul);
292
+ document.getElementById("download-json").addEventListener("click", () => {
293
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
294
+ downloadJSON(`data-${stamp}.json`, latestResult);
295
+ let sessionId = null;
296
+ });
297
+ });
298
+
299
+ // 地図
300
+ const map = L.map('map', {
301
+ preferCanvas:true,
302
+ zoomControl: true
303
+ }).setView([lat, lon], 12);
304
+ const baseLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
305
+ maxZoom: 19,
306
+ attribution: '&copy; OpenStreetMap contributors'
307
+ }).addTo(map);
308
+ /*
309
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
310
+ maxZoom: 19,
311
+ attribution: '&copy; OpenStreetMap contributors'
312
+ }).addTo(map);
313
+ */
314
+ // Drawn items
315
+ const drawnItems = new L.FeatureGroup();
316
+ map.addLayer(drawnItems);
317
+
318
+ // Draw control(矩形・ポリゴンのみ)
319
+ const drawControl = new L.Control.Draw({
320
+ position: 'topleft',
321
+ draw: {
322
+ polygon: {
323
+ allowIntersection: false,
324
+ showArea: true,
325
+ metric: true,
326
+ },
327
+ polyline: false,
328
+ marker: false,
329
+ circle: false,
330
+ circlemarker: false,
331
+ rectangle: {
332
+ shapeOptions: {
333
+ weight: 1,
334
+ fillOpacity: 0
335
+ }
336
+ }
337
+ },
338
+ /* edit: {
339
+ featureGroup: drawnItems,
340
+ edit: true,
341
+ remove: true
342
+ }*/
343
+ });
344
+ map.addControl(drawControl);
345
+
346
+ // レイヤ参照
347
+ let hexLayer = null;
348
+ let triLayer = null;
349
+ let myStartBtn = null;
350
+ let myLegend = null;
351
+ let myProgress = null;
352
+ let hexLayerIndex = {}; // hexId -> layer
353
+ let triLayerIndex = {}; // triangleId -> layer
354
+ let hexLabelLayer = L.layerGroup(); // Hex IDラベル群
355
+
356
+ // Util: Bounds → "minLon,minLat,maxLon,maxLat"
357
+ function boundsToBbox(bounds) {
358
+ return [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()].join(',');
359
+ }
360
+
361
+ // ハイライト制御
362
+ function clearHexHighlight() {
363
+ Object.values(hexLayerIndex).forEach(layer => {
364
+ layer.setStyle({
365
+ weight: 2,
366
+ opacity: 0.5,
367
+ fillOpacity: 0.1,
368
+ color: "#37474F",
369
+ fillColor: "#546E7A",
370
+ });
371
+ });
372
+ }
373
+
374
+ function highlightHexById(hexId, fillOpacity = 0.7, fillColor = "#3949AB") {
375
+ const layer = hexLayerIndex[hexId];
376
+ if (layer) {
377
+ layer.setStyle({
378
+ weight: 2,
379
+ opacity: 1,
380
+ fillOpacity: fillOpacity,
381
+ color: "#37474F",
382
+ fillColor: fillColor,
383
+ });
384
+ if (layer.bringToFront) layer.bringToFront();
385
+ }
386
+ }
387
+
388
+ function clearTriHighlight() {
389
+ Object.values(triLayerIndex).forEach(layer => {
390
+ layer.setStyle({
391
+ weight: 1,
392
+ opacity: 0.8,
393
+ fillOpacity: 0,
394
+ fillColor: "#EC407A",
395
+ color: "#F50057",
396
+ dashArray: "10 20",
397
+ dashOffset: 10,
398
+ });
399
+ });
400
+ }
401
+
402
+ function highlightTriangle(triId, style = {
403
+ weight: 1,
404
+ opacity: 1,
405
+ fillOpacity: 0.5,
406
+ fillColor: "#EC407A",
407
+ color: "#F50057",
408
+ dashArray: "15,30",
409
+ dashOffset: 0,
410
+ }) {
411
+ const layer = triLayerIndex[triId];
412
+ if (layer) {
413
+ layer.setStyle(style);
414
+ if (layer.bringToFront) layer.bringToFront();
415
+ }
416
+ }
417
+
418
+ // Hex IDラベルを作成
419
+ function renderHexLabels(hexFC) {
420
+ hexLabelLayer.clearLayers();
421
+ hexFC.features.forEach(f => {
422
+ const id = f.properties?.hexId;
423
+ if (!id) return;
424
+ const coords = f.geometry.coordinates[0];
425
+ let sumLat = 0,
426
+ sumLon = 0,
427
+ n = 0;
428
+ coords.slice(0, -1).forEach(([lon, lat]) => {
429
+ sumLat += lat;
430
+ sumLon += lon;
431
+ n++;
432
+ });
433
+ const lat = sumLat / n,
434
+ lon = sumLon / n;
435
+
436
+ const icon = L.divIcon({
437
+ html: `<div style="font-size:12px;font-weight:600;background:rgba(255,255,255,.85);padding:2px 4px;border-radius:4px;border:1px solid #999;">${id}</div>`,
438
+ className: '',
439
+ iconSize: [0, 0]
440
+ });
441
+ L.marker([lat, lon], {
442
+ icon
443
+ }).addTo(hexLabelLayer);
444
+ });
445
+ }
446
+
447
+ // Hex / Triangles を描画
448
+ function renderLayers(hexFC, trianglesFC) {
449
+ if (hexLayer) map.removeLayer(hexLayer);
450
+ if (triLayer) map.removeLayer(triLayer);
451
+ document.getElementById('map_legend')?.replaceChildren();
452
+ if (myLegend) map.removeControl(myLegend);
453
+ if (myStartBtn) map.removeControl(myStartBtn);
454
+ hexLabelLayer.removeFrom(map);
455
+ hexLayerIndex = {};
456
+ triLayerIndex = {};
457
+
458
+ // 六角形
459
+ hexLayer = L.geoJSON(hexFC, {
460
+ style: {
461
+ weight: 2,
462
+ opacity: 0.5,
463
+ fillOpacity: 0.1,
464
+ color: "#37474F",
465
+ fillColor: "#546E7A",
466
+ },
467
+ onEachFeature: (feature, layer) => {
468
+ const hid = feature.properties?.hexId;
469
+ if (hid != null) hexLayerIndex[hid] = layer;
470
+ const triIds = feature.properties?.triIds?.length ? feature.properties.triIds.join(', ') : 'N/A';
471
+ layer.bindPopup(`Hex ID: ${hid}<br>Triangles: ${triIds}`);
472
+ }
473
+ }).addTo(map);
474
+
475
+ const BottomLeftPanel = L.Control.extend({
476
+ options: {
477
+ position: 'bottomleft'
478
+ },
479
+ onAdd(map) {
480
+ const div = L.DomUtil.create('div', 'map-bottom-left-control');
481
+ div.innerHTML = `
482
+ <div class="panel-contents">
483
+ <button class="btn"id="doSomething">Start Crawling!</button>
484
+ </div>`;
485
+ // コントロール内の操作が地図ドラッグに奪われないようにする
486
+ L.DomEvent.disableClickPropagation(div);
487
+ L.DomEvent.disableScrollPropagation(div);
488
+
489
+ const btn = div.querySelector('#doSomething');
490
+ btn.addEventListener('click', (ev) => {
491
+ ev.preventDefault();
492
+ ev.stopPropagation(); // 念のため地図へのバブリングも止める
493
+ if (myStartBtn) map.removeControl(myStartBtn);
494
+ myProgress = new BottomLeftProgressBar().addTo(map);
495
+ // ---- 任意の処理 ----
496
+ socket.emit("crawling", {
497
+ sessionId: sessionId,
498
+ });
499
+ }, {
500
+ once: true
501
+ });
502
+
503
+ return div;
504
+ }
505
+ });
506
+ myStartBtn = new BottomLeftPanel().addTo(map);
507
+
508
+ // 三角形
509
+ triLayer = L.geoJSON(trianglesFC, {
510
+ style: {
511
+ weight: 1,
512
+ opacity: 0.8,
513
+ fillOpacity: 0,
514
+ fillColor: "#EC407A",
515
+ color: "#F50057",
516
+ dashArray: "15,30",
517
+ dashOffset: 0,
518
+ },
519
+ onEachFeature: (feature, layer) => {
520
+ const pid = feature.properties?.parentHexId ?? 'N/A';
521
+ const tid = feature.properties?.triangleId ?? 'N/A';
522
+ const cross = feature.properties?.crossNeighbors ?? [];
523
+ const neighborHexIds = feature.properties?.neighborHexIds ?? [];
524
+
525
+ triLayerIndex[tid] = layer;
526
+
527
+ layer.bindPopup(
528
+ `Triangle: ${tid}<br>` +
529
+ `Parent Hex: ${pid}<br>` +
530
+ `Cross neighbors: ${cross.length ? cross.join(', ') : 'None'}<br>` +
531
+ `Neighbor Hexes: ${neighborHexIds.length ? neighborHexIds.join(', ') : 'None'}`
532
+ );
533
+
534
+ layer.on('click', () => {
535
+ // クリック時:当該三角形+交差隣接三角形をハイライト
536
+ clearHexHighlight();
537
+ clearTriHighlight();
538
+ highlightHexById(pid); // 親Hexも強調
539
+ highlightTriangle(tid, {
540
+ weight: 3,
541
+ opacity: 1,
542
+ fillOpacity: 0.30
543
+ }); // クリック対象
544
+
545
+ // 隣接(別Hex)の三角形を同時に強調
546
+ cross.forEach(cid => {
547
+ highlightTriangle(cid, {
548
+ weight: 2,
549
+ opacity: 1,
550
+ fillOpacity: 0.25
551
+ });
552
+ // その親Hexも薄く強調
553
+ const neighborLayer = triLayerIndex[cid];
554
+ if (neighborLayer) {
555
+ const pHex = neighborLayer.feature?.properties?.parentHexId;
556
+ if (pHex != null) highlightHexById(pHex);
557
+ }
558
+ });
559
+ });
560
+
561
+ // マウスアウトで三角形のハイライトだけ戻す(Hexは維持)
562
+ layer.on('mouseout', () => {
563
+ clearTriHighlight();
564
+ });
565
+ }
566
+ });
567
+
568
+ // 三角形トグル
569
+ if (document.getElementById('toggleTriangles').checked) {
570
+ triLayer.addTo(map);
571
+ }
572
+
573
+ // Hex ID ラベル
574
+ renderHexLabels(hexFC);
575
+ if (document.getElementById('toggleHexLabels').checked) {
576
+ hexLabelLayer.addTo(map);
577
+ }
578
+
579
+ // ビュー調整
580
+ const layers = [hexLayer];
581
+ if (document.getElementById('toggleTriangles').checked) layers.push(triLayer);
582
+ const bounds = L.featureGroup(layers).getBounds();
583
+ if (bounds.isValid()) map.fitBounds(bounds, {
584
+ padding: [20, 20]
585
+ });
586
+ }
587
+
588
+ // API呼び出し
589
+ async function loadHexgridByBounds(bounds, drawn = null) {
590
+ const bbox = boundsToBbox(bounds);
591
+ document.getElementById('bboxLabel').textContent = `BBOX: ${bbox}`;
592
+
593
+ const cellSize = document.getElementById('cellSize').value.trim();
594
+ const units = document.getElementById('units').value.trim();
595
+ const keywords = document.getElementById('keywords').value.trim();
596
+ latestResult["_"]??={};
597
+ latestResult["_"]["keywords"] = keywords;
598
+ socket.once("hexgrid", (res) => {
599
+ if (res.sessionId) sessionId = res.sessionId;
600
+ //console.log(`hexgrid received (${res.hex.features.length} hexes, ${res.triangles.features.length} triangles)`);
601
+ const hex = res.hex;
602
+ const triangles = res.triangles;
603
+ latestResult["_"]["hex"] = hex;
604
+ latestResult["_"]["triangles"]=[triangles];
605
+ renderLayers(hex, triangles);
606
+ });
607
+ socket.emit("target", {
608
+ sessionId: sessionId,
609
+ query: {
610
+ bbox: bbox,
611
+ drawn: drawn,
612
+ cellSize: cellSize,
613
+ units: units,
614
+ tags: keywords
615
+ }
616
+ });
617
+ }
618
+
619
+ // Draw イベント
620
+ map.on(L.Draw.Event.CREATED, async (e) => {
621
+ drawnItems.clearLayers();
622
+ const layer = e.layer;
623
+ drawnItems.addLayer(layer);
624
+ try {
625
+ await loadHexgridByBounds(layer.getBounds(), layer.toGeoJSON());
626
+ } catch (err) {
627
+ alert(err.message);
628
+ }
629
+ });
630
+
631
+ map.on(L.Draw.Event.EDITSTOP, async () => {
632
+ const layers = drawnItems.getLayers();
633
+ if (layers.length === 1) {
634
+ try {
635
+ await loadHexgridByBounds(layers[0].getBounds(), layers[0].toGeoJSON());
636
+ } catch (err) {
637
+ alert(err.message);
638
+ }
639
+ }
640
+ });
641
+
642
+ map.on(L.Draw.Event.DELETED, () => {
643
+ drawnItems.clearLayers();
644
+ if (hexLayer) {
645
+ map.removeLayer(hexLayer);
646
+ hexLayer = null;
647
+ }
648
+ if (triLayer) {
649
+ map.removeLayer(triLayer);
650
+ triLayer = null;
651
+ }
652
+ if (myStartBtn) {
653
+ map.removeControl(myStartBtn);
654
+ myStartBtn = null;
655
+ }
656
+ if (myLegend) {
657
+ document.getElementById('map_legend')?.replaceChildren();
658
+ map.removeControl(myLegend);
659
+ myLegend = null;
660
+ }
661
+ hexLabelLayer.removeFrom(map);
662
+ document.getElementById('bboxLabel').textContent = '';
663
+ });
664
+
665
+ // ======= ハンバーガー / パネル制御 =======
666
+ const hamburger = document.getElementById('hamburger');
667
+ const sidepanel = document.getElementById('sidepanel');
668
+ const panelClose = document.getElementById('panelClose');
669
+ const panelOverlay = document.getElementById('panelOverlay');
670
+
671
+ function openPanel() {
672
+ sidepanel.classList.add('open');
673
+ hamburger.setAttribute('aria-expanded', 'true');
674
+ sidepanel.setAttribute('aria-hidden', 'false');
675
+ panelOverlay.hidden = false;
676
+ // 画面幅が狭い時に、Leafletのツールと干渉しないようパネルを最前面に
677
+ }
678
+
679
+ function closePanel() {
680
+ sidepanel.classList.remove('open');
681
+ hamburger.setAttribute('aria-expanded', 'false');
682
+ sidepanel.setAttribute('aria-hidden', 'true');
683
+ panelOverlay.hidden = true;
684
+ }
685
+
686
+ hamburger.addEventListener('click', () => {
687
+ if (sidepanel.classList.contains('open')) closePanel();
688
+ else openPanel();
689
+ });
690
+ panelClose.addEventListener('click', closePanel);
691
+ panelOverlay.addEventListener('click', closePanel);
692
+ // ESCで閉じる
693
+ document.addEventListener('keydown', (e) => {
694
+ if (e.key === 'Escape') closePanel();
695
+ });
696
+
697
+ // UIトグル
698
+ document.getElementById('clear').addEventListener('click', () => {
699
+ drawnItems.clearLayers();
700
+ if (hexLayer) {
701
+ map.removeLayer(hexLayer);
702
+ hexLayer = null;
703
+ }
704
+ if (triLayer) {
705
+ map.removeLayer(triLayer);
706
+ triLayer = null;
707
+ }
708
+ if (myStartBtn) {
709
+ map.removeControl(myStartBtn);
710
+ myStartBtn = null;
711
+ }
712
+ if (myLegend) {
713
+ document.getElementById('map_legend')?.replaceChildren();
714
+ map.removeControl(myLegend);
715
+ myLegend = null;
716
+ }
717
+ hexLabelLayer.removeFrom(map);
718
+ document.getElementById('bboxLabel').textContent = '';
719
+ });
720
+
721
+ document.getElementById('toggleTriangles').addEventListener('change', () => {
722
+ if (!triLayer) return;
723
+ if (document.getElementById('toggleTriangles').checked) triLayer.addTo(map);
724
+ else map.removeLayer(triLayer);
725
+ });
726
+
727
+ document.getElementById('toggleHexLabels').addEventListener('change', () => {
728
+ if (document.getElementById('toggleHexLabels').checked) {
729
+ hexLabelLayer.addTo(map);
730
+ } else {
731
+ hexLabelLayer.removeFrom(map);
732
+ }
733
+ });
734
+
735
+ // 初期は描画待ち(必要なら初期BBOXを有効化)
736
+ // loadHexgridByBounds(L.latLngBounds([35.53, 139.55], [35.80, 139.92]));
737
+ </script>
738
+ </body>
739
+
740
+ </html>