splatone 0.0.22 → 0.0.23
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/.API_KEY.Flickr +1 -0
- package/README.md +60 -11
- package/browse.js +8 -0
- package/color.js +360 -4
- package/crawler.js +448 -131
- package/package.json +3 -2
- package/plugins/flickr/worker.js +6 -27
- package/public/out/.gitkeep +0 -0
- package/public/out/result.nzzxvl24mi420u0v.json +1 -0
- package/public/out/voronoi/.gitkeep +0 -0
- package/publication/README.md +18 -0
- package/publication/main.tex +85 -0
- package/publication/references.bib +6 -0
- package/views/index.ejs +686 -343
- package/.vscode/mcp.json +0 -12
- package/.vscode/settings.json +0 -7
- package/assets/icon_data_export.png +0 -0
- package/assets/icon_image_download.png +0 -0
- package/assets/screenshot_florida_hex_majorityr.png +0 -0
- package/assets/screenshot_massive_points_bulky.png +0 -0
- package/assets/screenshot_pie_tokyo.png +0 -0
- package/assets/screenshot_sea-mountain_bulky.png +0 -0
- package/assets/screenshot_venice_heat.png +0 -0
- package/assets/screenshot_venice_marker-cluster.png +0 -0
- package/assets/screenshot_venice_simple.png +0 -0
- package/assets/screenshot_voronoi_tokyo.png +0 -0
package/views/index.ejs
CHANGED
|
@@ -31,10 +31,14 @@
|
|
|
31
31
|
|
|
32
32
|
<div class="panel-body">
|
|
33
33
|
<section class="panel-section">
|
|
34
|
+
<% if (browseMode) { %>
|
|
35
|
+
<p class="muted">browseモードでは地図の閲覧のみ可能です。範囲描画とクロール開始は無効化されています。</p>
|
|
36
|
+
<% } else { %>
|
|
34
37
|
<p class="muted">
|
|
35
38
|
地図上で <strong>矩形をドラッグ</strong> して BBOX を指定してください。<br>
|
|
36
39
|
クリックした三角形と、<em>共有辺を持つ別Hexの三角形</em>が同時にハイライトされます。
|
|
37
40
|
</p>
|
|
41
|
+
<% } %>
|
|
38
42
|
</section>
|
|
39
43
|
|
|
40
44
|
<section class="panel-section">
|
|
@@ -56,23 +60,25 @@
|
|
|
56
60
|
</select>
|
|
57
61
|
</label>
|
|
58
62
|
|
|
59
|
-
<
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
<div class="bbox-row">
|
|
64
|
+
<span id="bboxLabel" class="bbox-label"></span>
|
|
65
|
+
</div>
|
|
66
|
+
</section>
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<span>
|
|
68
|
+
<section class="panel-section">
|
|
69
|
+
<label class="field">
|
|
70
|
+
<span>CLI Command</span>
|
|
71
|
+
<textarea id="cliCommand" class="cli-command" rows="4" readonly spellcheck="false"></textarea>
|
|
67
72
|
</label>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<button id="clear" class="btn">Clear</button>
|
|
73
|
+
<div class="copy-row">
|
|
74
|
+
<button id="copyCliCommand" class="btn" type="button">Copy to clipboard</button>
|
|
71
75
|
</div>
|
|
76
|
+
</section>
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
<section class="panel-section" id="statsSection" hidden>
|
|
79
|
+
<h3>統計情報</h3>
|
|
80
|
+
<div id="statsSummary" class="stats-summary muted"></div>
|
|
81
|
+
<div id="statsHexList" class="stats-hex-list"></div>
|
|
76
82
|
</section>
|
|
77
83
|
|
|
78
84
|
<section class="panel-section">
|
|
@@ -91,6 +97,15 @@
|
|
|
91
97
|
<!-- パネルの背面オーバーレイ -->
|
|
92
98
|
<div id="panelOverlay" class="panel-overlay" hidden></div>
|
|
93
99
|
|
|
100
|
+
<% if (browseMode) { %>
|
|
101
|
+
<div id="dropOverlay" class="drop-overlay" hidden>
|
|
102
|
+
<div class="drop-overlay__inner">
|
|
103
|
+
<p class="drop-overlay__title">結果ファイルをドロップ</p>
|
|
104
|
+
<p class="drop-overlay__subtitle">crawlerのresult*.jsonやエクスポートJSONを読み込めます</p>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<% } %>
|
|
108
|
+
|
|
94
109
|
<!-- 地図 -->
|
|
95
110
|
<div id="map"></div>
|
|
96
111
|
|
|
@@ -111,6 +126,13 @@
|
|
|
111
126
|
<script type="module">
|
|
112
127
|
const lat = <%= lat %>;
|
|
113
128
|
const lon = <%= lon %>;
|
|
129
|
+
const defaultGeometry = <%- JSON.stringify(defaultGeometry || {}) %>;
|
|
130
|
+
const browseMode = <%- JSON.stringify(Boolean(browseMode)) %>;
|
|
131
|
+
const cliContext = {
|
|
132
|
+
baseCommand: <%- JSON.stringify(cliBaseCommand || '') %>,
|
|
133
|
+
plugin: <%- JSON.stringify(selectedPlugin || '') %>,
|
|
134
|
+
visualizers: <%- JSON.stringify(selectedVisualizers || []) %>,
|
|
135
|
+
};
|
|
114
136
|
const socket = io({
|
|
115
137
|
path: "/socket",
|
|
116
138
|
reconnection: false
|
|
@@ -155,6 +177,54 @@
|
|
|
155
177
|
if (label) label.textContent = v + '%';
|
|
156
178
|
}
|
|
157
179
|
|
|
180
|
+
function formatPercentValue(value) {
|
|
181
|
+
if (!Number.isFinite(value)) return '0%';
|
|
182
|
+
return (Math.round(Math.max(0, Math.min(1, value)) * 1000) / 10) + '%';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function renderStatsPanel(stats) {
|
|
186
|
+
const section = document.getElementById('statsSection');
|
|
187
|
+
const summaryEl = document.getElementById('statsSummary');
|
|
188
|
+
const hexListEl = document.getElementById('statsHexList');
|
|
189
|
+
if (!section || !summaryEl || !hexListEl) return;
|
|
190
|
+
if (!stats || !stats.totals) {
|
|
191
|
+
section.hidden = true;
|
|
192
|
+
summaryEl.textContent = '';
|
|
193
|
+
hexListEl.textContent = '';
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const totals = stats.totals;
|
|
197
|
+
section.hidden = false;
|
|
198
|
+
summaryEl.innerHTML = `
|
|
199
|
+
<div>Hex総数: <strong>${totals.hexes ?? 0}</strong></div>
|
|
200
|
+
<div>カテゴリ数: <strong>${totals.categories ?? 0}</strong></div>
|
|
201
|
+
<div>取得済み: <strong>${totals.crawled ?? 0}</strong></div>
|
|
202
|
+
<div>残り推定: <strong>${totals.remaining ?? 0}</strong></div>
|
|
203
|
+
<div>進捗: <strong>${formatPercentValue(totals.percent ?? 0)}</strong></div>
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
const entries = Object.entries(stats.hexes ?? {});
|
|
207
|
+
if (!entries.length) {
|
|
208
|
+
hexListEl.innerHTML = '<p class="muted">Hex単位の統計はまだありません。</p>';
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
entries.sort((a, b) => (b[1]?.percent ?? 0) - (a[1]?.percent ?? 0));
|
|
212
|
+
const rows = entries.slice(0, 6).map(([hexId, info]) => {
|
|
213
|
+
const percent = formatPercentValue(info?.percent ?? 0);
|
|
214
|
+
const crawled = info?.crawled ?? 0;
|
|
215
|
+
const expected = info?.expected ?? info?.total ?? (crawled + (info?.remaining ?? 0));
|
|
216
|
+
const remaining = info?.remaining ?? Math.max(0, expected - crawled);
|
|
217
|
+
return `
|
|
218
|
+
<div class="stats-hex-row">
|
|
219
|
+
<span class="stats-hex-label">Hex ${hexId}</span>
|
|
220
|
+
<span class="stats-hex-progress">${percent}</span>
|
|
221
|
+
<span class="stats-hex-detail">${crawled}/${expected} (残り${remaining})</span>
|
|
222
|
+
</div>
|
|
223
|
+
`;
|
|
224
|
+
});
|
|
225
|
+
hexListEl.innerHTML = rows.join('');
|
|
226
|
+
}
|
|
227
|
+
|
|
158
228
|
function downloadJSON(filename, obj) {
|
|
159
229
|
const json = JSON.stringify(obj, null, 2);
|
|
160
230
|
const blob = new Blob([json], {
|
|
@@ -170,6 +240,89 @@
|
|
|
170
240
|
URL.revokeObjectURL(url);
|
|
171
241
|
}
|
|
172
242
|
|
|
243
|
+
let currentDrawnGeoJSON = null;
|
|
244
|
+
let currentDrawKind = null;
|
|
245
|
+
let currentBboxArray = null;
|
|
246
|
+
|
|
247
|
+
const cliTextarea = document.getElementById('cliCommand');
|
|
248
|
+
const copyCliButton = document.getElementById('copyCliCommand');
|
|
249
|
+
|
|
250
|
+
function quoteSingle(value) {
|
|
251
|
+
const safe = String(value ?? '').replace(/'/g, "''");
|
|
252
|
+
return "'" + safe + "'";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildCliCommandString() {
|
|
256
|
+
if (!cliContext?.baseCommand) return '';
|
|
257
|
+
const parts = [cliContext.baseCommand.trim()].filter(Boolean);
|
|
258
|
+
if (cliContext.plugin) {
|
|
259
|
+
parts.push('-p', cliContext.plugin);
|
|
260
|
+
}
|
|
261
|
+
if (Array.isArray(cliContext.visualizers)) {
|
|
262
|
+
cliContext.visualizers.forEach((vis) => {
|
|
263
|
+
if (vis) parts.push(`--vis-${vis}`);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const keywordsInput = document.getElementById('keywords');
|
|
267
|
+
const keywordsVal = keywordsInput?.value?.trim();
|
|
268
|
+
if (keywordsVal) {
|
|
269
|
+
parts.push('-k', quoteSingle(keywordsVal));
|
|
270
|
+
}
|
|
271
|
+
const cellSizeInput = document.getElementById('cellSize');
|
|
272
|
+
const cellSizeVal = cellSizeInput?.value?.trim();
|
|
273
|
+
if (cellSizeVal) {
|
|
274
|
+
parts.push('--ui-cell-size', cellSizeVal);
|
|
275
|
+
}
|
|
276
|
+
const unitsSelect = document.getElementById('units');
|
|
277
|
+
const unitsVal = unitsSelect?.value;
|
|
278
|
+
if (unitsVal) {
|
|
279
|
+
parts.push('--ui-units', unitsVal);
|
|
280
|
+
}
|
|
281
|
+
if (currentDrawKind === 'rectangle' && Array.isArray(currentBboxArray)) {
|
|
282
|
+
parts.push('--ui-bbox', currentBboxArray.join(','));
|
|
283
|
+
} else if (currentDrawnGeoJSON) {
|
|
284
|
+
parts.push('--ui-polygon', quoteSingle(JSON.stringify(currentDrawnGeoJSON)));
|
|
285
|
+
}
|
|
286
|
+
return parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function updateCliCommand() {
|
|
290
|
+
if (!cliTextarea) return;
|
|
291
|
+
cliTextarea.value = buildCliCommandString();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
copyCliButton?.addEventListener('click', async () => {
|
|
295
|
+
if (!cliTextarea) return;
|
|
296
|
+
const text = cliTextarea.value;
|
|
297
|
+
try {
|
|
298
|
+
if (navigator.clipboard?.writeText) {
|
|
299
|
+
await navigator.clipboard.writeText(text);
|
|
300
|
+
} else {
|
|
301
|
+
cliTextarea.focus();
|
|
302
|
+
cliTextarea.select();
|
|
303
|
+
document.execCommand('copy');
|
|
304
|
+
window.getSelection()?.removeAllRanges?.();
|
|
305
|
+
}
|
|
306
|
+
Toastify({
|
|
307
|
+
text: 'CLIコマンドをコピーしました',
|
|
308
|
+
duration: 3000,
|
|
309
|
+
gravity: 'bottom',
|
|
310
|
+
position: 'center',
|
|
311
|
+
className: 'toast-info'
|
|
312
|
+
}).showToast();
|
|
313
|
+
} catch (err) {
|
|
314
|
+
Toastify({
|
|
315
|
+
text: `コピーに失敗しました: ${err?.message || err}`,
|
|
316
|
+
duration: 4000,
|
|
317
|
+
gravity: 'bottom',
|
|
318
|
+
position: 'center',
|
|
319
|
+
className: 'toast-error'
|
|
320
|
+
}).showToast();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
updateCliCommand();
|
|
325
|
+
|
|
173
326
|
socket.on('connect', () => {
|
|
174
327
|
//console.log('Connected to server');
|
|
175
328
|
});
|
|
@@ -178,11 +331,20 @@
|
|
|
178
331
|
//console.log('Disconnected from server');
|
|
179
332
|
});
|
|
180
333
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
334
|
+
document.getElementById('keywords')?.addEventListener('input', updateCliCommand);
|
|
335
|
+
document.getElementById('cellSize')?.addEventListener('input', updateCliCommand);
|
|
336
|
+
document.getElementById('units')?.addEventListener('change', updateCliCommand);
|
|
337
|
+
|
|
338
|
+
let sessionId = null;
|
|
339
|
+
let visualizers = {};
|
|
340
|
+
let results = {};
|
|
341
|
+
let files = {};
|
|
342
|
+
let layerControl = null;
|
|
343
|
+
const hexProgress = {};
|
|
344
|
+
let welcomeReadyResolve;
|
|
345
|
+
const welcomeReady = new Promise((resolve) => {
|
|
346
|
+
welcomeReadyResolve = resolve;
|
|
347
|
+
});
|
|
186
348
|
socket.on("welcome", async (res) => {
|
|
187
349
|
//console.log(`welcome ${res.sessionId} at ${new Date(res.time).toLocaleTimeString()}`);
|
|
188
350
|
//console.log("VIS", res.visualizers);
|
|
@@ -193,6 +355,10 @@
|
|
|
193
355
|
return [vis, m.entry ?? m.default ?? m[vis]];
|
|
194
356
|
})));
|
|
195
357
|
//console.log(visualizers);
|
|
358
|
+
if (welcomeReadyResolve) {
|
|
359
|
+
welcomeReadyResolve();
|
|
360
|
+
welcomeReadyResolve = null;
|
|
361
|
+
}
|
|
196
362
|
});
|
|
197
363
|
|
|
198
364
|
function generateLegend(legends) {
|
|
@@ -251,28 +417,35 @@
|
|
|
251
417
|
}
|
|
252
418
|
});
|
|
253
419
|
socket.on('progress', (res) => {
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
420
|
+
//res.currentHexはカテゴリごとのcrawled/totalを保持している
|
|
421
|
+
const { crawled, total } = Object.values(res.currentHex ?? {}).reduce(
|
|
422
|
+
(acc, cat) => (cat == null ? acc : {
|
|
423
|
+
crawled: acc.crawled + (cat.crawled ?? 0),
|
|
424
|
+
total: acc.total + (cat.total ?? 0)
|
|
425
|
+
}),
|
|
426
|
+
{ crawled: 0, total: 0 }
|
|
427
|
+
);
|
|
428
|
+
const safeTotal = Math.max(1, total);
|
|
429
|
+
const normalized = total === 0 ? 1 : Math.min(1, crawled / safeTotal);
|
|
430
|
+
hexProgress[res.hexId] = {
|
|
257
431
|
crawled,
|
|
258
|
-
total
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
crawled: 0,
|
|
265
|
-
total: 0
|
|
266
|
-
}
|
|
432
|
+
total: safeTotal,
|
|
433
|
+
normalized
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const aggregated = Object.values(hexProgress).reduce(
|
|
437
|
+
(acc, hex) => ({
|
|
438
|
+
crawled: acc.crawled + (hex?.crawled ?? 0),
|
|
439
|
+
total: acc.total + (hex?.total ?? 0)
|
|
440
|
+
}),
|
|
441
|
+
{ crawled: 0, total: 0 }
|
|
267
442
|
);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const percent = Math.round((crawled / total) * 100000) / 1000;
|
|
272
|
-
//console.log(`[Done] ${crawled} / ${total} --> ${percent}`);
|
|
443
|
+
const aggSafeTotal = Math.max(1, aggregated.total);
|
|
444
|
+
const overallNormalized = aggregated.total === 0 ? 1 : Math.min(1, aggregated.crawled / aggSafeTotal);
|
|
445
|
+
const percent = Math.round(overallNormalized * 100000) / 1000;
|
|
273
446
|
setProgress(document.getElementById("progressCrawl"), percent);
|
|
274
447
|
//ここでHexのプログレスグラデーションレイヤのOpacity調整
|
|
275
|
-
highlightHexById(res.hexId, 1 -
|
|
448
|
+
highlightHexById(res.hexId, 1 - normalized, "#263238");
|
|
276
449
|
});
|
|
277
450
|
|
|
278
451
|
function isPlainObject(a) {
|
|
@@ -348,79 +521,171 @@
|
|
|
348
521
|
}); // only one argument is expected
|
|
349
522
|
});
|
|
350
523
|
|
|
351
|
-
let latestResult =
|
|
524
|
+
let latestResult = null;
|
|
352
525
|
let latestFile = null;
|
|
353
526
|
const overlays = {};
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
527
|
+
const resultExtras = {};
|
|
528
|
+
|
|
529
|
+
async function visualizeBundle(bundle, { sourceLabel = 'server' } = {}) {
|
|
530
|
+
await welcomeReady;
|
|
531
|
+
if (!bundle || typeof bundle !== 'object') {
|
|
532
|
+
Toastify({
|
|
533
|
+
text: '結果データを読み込めませんでした',
|
|
534
|
+
duration: 4000,
|
|
535
|
+
gravity: 'bottom',
|
|
536
|
+
position: 'center',
|
|
537
|
+
className: 'toast-error'
|
|
538
|
+
}).showToast();
|
|
539
|
+
return false;
|
|
363
540
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
541
|
+
|
|
542
|
+
clearHexHighlight();
|
|
543
|
+
const geoJson = bundle.geoJson ?? {};
|
|
544
|
+
const palette = bundle.palette ?? {};
|
|
545
|
+
const visOptionsFromServer = bundle.visOptions ?? {};
|
|
546
|
+
|
|
547
|
+
renderStatsPanel(bundle.context?.stats ?? null);
|
|
548
|
+
bundle.extra = {
|
|
549
|
+
...(bundle.extra ?? {}),
|
|
550
|
+
...resultExtras
|
|
367
551
|
};
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
552
|
+
latestResult = bundle;
|
|
553
|
+
latestFile = null;
|
|
554
|
+
|
|
555
|
+
const bundleHexGrid = bundle.context?.hexGrid ?? bundle.extra?.hex ?? null;
|
|
556
|
+
let bundleTriangles = bundle.context?.triangles ?? null;
|
|
557
|
+
if (!bundleTriangles) {
|
|
558
|
+
const extraTriangles = bundle.extra?.triangles;
|
|
559
|
+
if (Array.isArray(extraTriangles)) {
|
|
560
|
+
bundleTriangles = extraTriangles.find((entry) => entry?.type === 'FeatureCollection') ?? null;
|
|
561
|
+
} else if (extraTriangles?.type === 'FeatureCollection') {
|
|
562
|
+
bundleTriangles = extraTriangles;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (bundleHexGrid || bundleTriangles) {
|
|
566
|
+
refreshGridLayers(bundleHexGrid ?? null, bundleTriangles ?? null, { addToMap: false });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (layerControl) {
|
|
570
|
+
map.removeControl(layerControl);
|
|
571
|
+
layerControl = null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
Object.keys(overlays).forEach((key) => {
|
|
575
|
+
const layer = overlays[key];
|
|
576
|
+
if (!layer) return;
|
|
577
|
+
if (typeof layer.remove === 'function') {
|
|
578
|
+
try { layer.remove(); } catch (err) { console.warn('failed to remove layer', err); }
|
|
579
|
+
} else if (typeof layer.removeFrom === 'function') {
|
|
580
|
+
try { layer.removeFrom(map); } catch (err) { console.warn('failed to remove layer', err); }
|
|
581
|
+
} else if (layer instanceof L.Layer) {
|
|
582
|
+
try { map.removeLayer(layer); } catch (err) { console.warn('failed to remove layer', err); }
|
|
583
|
+
}
|
|
584
|
+
delete overlays[key];
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
for (const vis of Object.keys(visualizers)) {
|
|
588
|
+
const handler = visualizers[vis];
|
|
589
|
+
if (typeof handler !== 'function') continue;
|
|
590
|
+
let layers = null;
|
|
591
|
+
try {
|
|
592
|
+
layers = await handler(map, geoJson[vis], {
|
|
593
|
+
palette,
|
|
594
|
+
visOptions: visOptionsFromServer[vis]
|
|
595
|
+
});
|
|
596
|
+
} catch (err) {
|
|
597
|
+
console.error(`[visualizer:${vis}] failed`, err);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (!layers) continue;
|
|
601
|
+
if (isPlainObject(layers) && !layers.type) {
|
|
380
602
|
for (const name in layers) {
|
|
381
|
-
//console.log("Lay", vis, name);
|
|
382
603
|
overlays[`[${vis}] ${name}`] = layers[name];
|
|
383
604
|
}
|
|
605
|
+
} else {
|
|
606
|
+
overlays[`[${vis}]`] = layers;
|
|
384
607
|
}
|
|
385
608
|
}
|
|
386
|
-
|
|
387
|
-
if (myProgress)
|
|
388
|
-
|
|
609
|
+
|
|
610
|
+
if (myProgress) {
|
|
611
|
+
map.removeControl(myProgress);
|
|
612
|
+
myProgress = null;
|
|
613
|
+
}
|
|
614
|
+
if (myLegend) {
|
|
615
|
+
map.removeControl(myLegend);
|
|
616
|
+
myLegend = null;
|
|
617
|
+
}
|
|
389
618
|
document.getElementById('map_legend')?.replaceChildren();
|
|
619
|
+
myLegend = new BottomLeftLegend().addTo(map);
|
|
390
620
|
const ul = document.createElement('div');
|
|
391
621
|
ul.className = 'legend_ul';
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
622
|
+
const paletteEntries = Object.entries(palette).map(([category, colors]) => ({
|
|
623
|
+
category,
|
|
624
|
+
fill: colors?.color ?? '#ffffff',
|
|
625
|
+
stroke: colors?.darken ?? colors?.color ?? '#ffffff'
|
|
626
|
+
}));
|
|
627
|
+
ul.innerHTML = generateLegend(paletteEntries);
|
|
628
|
+
document.getElementById('map_legend')?.appendChild(ul);
|
|
629
|
+
|
|
630
|
+
const overlayOptions = { ...overlays };
|
|
631
|
+
if (drawnItems && drawnItems.getLayers().length > 0) {
|
|
632
|
+
overlayOptions['Boundary'] = drawnItems;
|
|
633
|
+
}
|
|
634
|
+
if (hexLayer) {
|
|
635
|
+
overlayOptions['Hex Grid'] = hexLayer;
|
|
636
|
+
}
|
|
637
|
+
if (triLayer) {
|
|
638
|
+
overlayOptions['Tri Grid'] = triLayer;
|
|
399
639
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
"Hex Grid": hexLayer,
|
|
406
|
-
"Tri Grid": triLayer
|
|
407
|
-
}, {
|
|
640
|
+
|
|
641
|
+
if (layerControl) {
|
|
642
|
+
map.removeControl(layerControl);
|
|
643
|
+
}
|
|
644
|
+
layerControl = L.control.layers([baseLayer], overlayOptions, {
|
|
408
645
|
position: 'topleft'
|
|
409
646
|
});
|
|
410
647
|
layerControl.addTo(map);
|
|
411
|
-
|
|
412
|
-
document.getElementById(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
648
|
+
|
|
649
|
+
const downloadBtn = document.getElementById('download-json');
|
|
650
|
+
if (downloadBtn && downloadBtn.dataset.bound !== '1') {
|
|
651
|
+
downloadBtn.dataset.bound = '1';
|
|
652
|
+
downloadBtn.addEventListener('click', async () => {
|
|
653
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
654
|
+
if (latestFile === null) {
|
|
655
|
+
if (latestResult) {
|
|
656
|
+
downloadJSON(`splatone-${stamp}.json`, latestResult);
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
await downloadJSONFile(`splatone-${stamp}.json`, latestFile);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
Toastify({
|
|
665
|
+
text: `結果を読み込みました (${sourceLabel})`,
|
|
666
|
+
duration: 2600,
|
|
667
|
+
gravity: 'bottom',
|
|
668
|
+
position: 'center',
|
|
669
|
+
className: 'toast-success'
|
|
670
|
+
}).showToast();
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
socket.on('result', async (res, callback) => {
|
|
674
|
+
let bundle = res.bundle ?? null;
|
|
675
|
+
if (!bundle) {
|
|
676
|
+
bundle = results[res.resultId];
|
|
677
|
+
} else {
|
|
678
|
+
results[res.resultId] = bundle;
|
|
679
|
+
}
|
|
680
|
+
if (!bundle) {
|
|
681
|
+
console.warn('[result] bundle not available yet');
|
|
682
|
+
callback({ pong: Date.now(), ok: false });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const success = await visualizeBundle(bundle, { sourceLabel: 'server' });
|
|
421
686
|
callback({
|
|
422
687
|
pong: Date.now(),
|
|
423
|
-
ok:
|
|
688
|
+
ok: Boolean(success)
|
|
424
689
|
});
|
|
425
690
|
});
|
|
426
691
|
|
|
@@ -456,32 +721,30 @@
|
|
|
456
721
|
map.addLayer(drawnItems);
|
|
457
722
|
|
|
458
723
|
// Draw control(矩形・ポリゴンのみ)
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
724
|
+
let drawControl = null;
|
|
725
|
+
if (!browseMode) {
|
|
726
|
+
drawControl = new L.Control.Draw({
|
|
727
|
+
position: 'topleft',
|
|
728
|
+
draw: {
|
|
729
|
+
polygon: {
|
|
730
|
+
allowIntersection: false,
|
|
731
|
+
showArea: true,
|
|
732
|
+
metric: true,
|
|
733
|
+
},
|
|
734
|
+
polyline: false,
|
|
735
|
+
marker: false,
|
|
736
|
+
circle: false,
|
|
737
|
+
circlemarker: false,
|
|
738
|
+
rectangle: {
|
|
739
|
+
shapeOptions: {
|
|
740
|
+
weight: 1,
|
|
741
|
+
fillOpacity: 0
|
|
742
|
+
}
|
|
475
743
|
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
edit: true,
|
|
481
|
-
remove: true
|
|
482
|
-
}*/
|
|
483
|
-
});
|
|
484
|
-
map.addControl(drawControl);
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
map.addControl(drawControl);
|
|
747
|
+
}
|
|
485
748
|
|
|
486
749
|
//印刷
|
|
487
750
|
L.easyPrint({
|
|
@@ -500,7 +763,6 @@
|
|
|
500
763
|
let myProgress = null;
|
|
501
764
|
let hexLayerIndex = {}; // hexId -> layer
|
|
502
765
|
let triLayerIndex = {}; // triangleId -> layer
|
|
503
|
-
let hexLabelLayer = L.layerGroup(); // Hex IDラベル群
|
|
504
766
|
|
|
505
767
|
// Util: Bounds → "minLon,minLat,maxLon,maxLat"
|
|
506
768
|
function boundsToBbox(bounds) {
|
|
@@ -564,193 +826,185 @@
|
|
|
564
826
|
}
|
|
565
827
|
}
|
|
566
828
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
829
|
+
function refreshGridLayers(hexFC = null, trianglesFC = null, { addToMap = true } = {}) {
|
|
830
|
+
const hexWasVisible = hexLayer ? map.hasLayer(hexLayer) : false;
|
|
831
|
+
const triWasVisible = triLayer ? map.hasLayer(triLayer) : false;
|
|
832
|
+
|
|
833
|
+
if (hexLayer) {
|
|
834
|
+
map.removeLayer(hexLayer);
|
|
835
|
+
hexLayer = null;
|
|
836
|
+
}
|
|
837
|
+
if (triLayer) {
|
|
838
|
+
map.removeLayer(triLayer);
|
|
839
|
+
triLayer = null;
|
|
840
|
+
}
|
|
841
|
+
hexLayerIndex = {};
|
|
842
|
+
triLayerIndex = {};
|
|
843
|
+
|
|
844
|
+
if (hexFC) {
|
|
845
|
+
const nextHexLayer = L.geoJSON(hexFC, {
|
|
846
|
+
style: {
|
|
847
|
+
weight: 2,
|
|
848
|
+
opacity: 0.5,
|
|
849
|
+
fillOpacity: 0.1,
|
|
850
|
+
color: "#37474F",
|
|
851
|
+
fillColor: "#546E7A",
|
|
852
|
+
},
|
|
853
|
+
onEachFeature: (feature, layer) => {
|
|
854
|
+
const hid = feature.properties?.hexId;
|
|
855
|
+
if (hid != null) hexLayerIndex[hid] = layer;
|
|
856
|
+
const triIds = feature.properties?.triIds?.length ? feature.properties.triIds.join(', ') : 'N/A';
|
|
857
|
+
layer.bindPopup(`Hex ID: ${hid}<br>Triangles: ${triIds}`);
|
|
858
|
+
}
|
|
581
859
|
});
|
|
582
|
-
|
|
583
|
-
|
|
860
|
+
hexLayer = nextHexLayer;
|
|
861
|
+
if (addToMap || hexWasVisible) {
|
|
862
|
+
nextHexLayer.addTo(map);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (trianglesFC) {
|
|
867
|
+
const nextTriLayer = L.geoJSON(trianglesFC, {
|
|
868
|
+
style: {
|
|
869
|
+
weight: 1,
|
|
870
|
+
opacity: 0.8,
|
|
871
|
+
fillOpacity: 0,
|
|
872
|
+
fillColor: "#EC407A",
|
|
873
|
+
color: "#F50057",
|
|
874
|
+
dashArray: "15,30",
|
|
875
|
+
dashOffset: 0,
|
|
876
|
+
},
|
|
877
|
+
onEachFeature: (feature, layer) => {
|
|
878
|
+
const pid = feature.properties?.parentHexId ?? 'N/A';
|
|
879
|
+
const tid = feature.properties?.triangleId ?? 'N/A';
|
|
880
|
+
const cross = feature.properties?.crossNeighbors ?? [];
|
|
881
|
+
const neighborHexIds = feature.properties?.neighborHexIds ?? [];
|
|
882
|
+
|
|
883
|
+
triLayerIndex[tid] = layer;
|
|
884
|
+
|
|
885
|
+
layer.bindPopup(
|
|
886
|
+
`Triangle: ${tid}<br>` +
|
|
887
|
+
`Parent Hex: ${pid}<br>` +
|
|
888
|
+
`Cross neighbors: ${cross.length ? cross.join(', ') : 'None'}<br>` +
|
|
889
|
+
`Neighbor Hexes: ${neighborHexIds.length ? neighborHexIds.join(', ') : 'None'}`
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
layer.on('click', () => {
|
|
893
|
+
clearHexHighlight();
|
|
894
|
+
clearTriHighlight();
|
|
895
|
+
highlightHexById(pid);
|
|
896
|
+
highlightTriangle(tid, {
|
|
897
|
+
weight: 3,
|
|
898
|
+
opacity: 1,
|
|
899
|
+
fillOpacity: 0.30
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
cross.forEach(cid => {
|
|
903
|
+
highlightTriangle(cid, {
|
|
904
|
+
weight: 2,
|
|
905
|
+
opacity: 1,
|
|
906
|
+
fillOpacity: 0.25
|
|
907
|
+
});
|
|
908
|
+
const neighborLayer = triLayerIndex[cid];
|
|
909
|
+
if (neighborLayer) {
|
|
910
|
+
const pHex = neighborLayer.feature?.properties?.parentHexId;
|
|
911
|
+
if (pHex != null) highlightHexById(pHex);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
});
|
|
584
915
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
916
|
+
layer.on('mouseout', () => {
|
|
917
|
+
clearTriHighlight();
|
|
918
|
+
});
|
|
919
|
+
}
|
|
589
920
|
});
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
921
|
+
triLayer = nextTriLayer;
|
|
922
|
+
if (addToMap || triWasVisible) {
|
|
923
|
+
nextTriLayer.addTo(map);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
594
926
|
}
|
|
595
927
|
|
|
596
928
|
// Hex / Triangles を描画
|
|
597
929
|
function renderLayers(hexFC, trianglesFC) {
|
|
598
|
-
if (hexLayer) map.removeLayer(hexLayer);
|
|
599
|
-
if (triLayer) map.removeLayer(triLayer);
|
|
600
930
|
document.getElementById('map_legend')?.replaceChildren();
|
|
601
931
|
if (myLegend) map.removeControl(myLegend);
|
|
602
932
|
if (myStartBtn) map.removeControl(myStartBtn);
|
|
603
|
-
hexLabelLayer.removeFrom(map);
|
|
604
|
-
hexLayerIndex = {};
|
|
605
|
-
triLayerIndex = {};
|
|
606
933
|
|
|
607
|
-
|
|
608
|
-
hexLayer = L.geoJSON(hexFC, {
|
|
609
|
-
style: {
|
|
610
|
-
weight: 2,
|
|
611
|
-
opacity: 0.5,
|
|
612
|
-
fillOpacity: 0.1,
|
|
613
|
-
color: "#37474F",
|
|
614
|
-
fillColor: "#546E7A",
|
|
615
|
-
},
|
|
616
|
-
onEachFeature: (feature, layer) => {
|
|
617
|
-
const hid = feature.properties?.hexId;
|
|
618
|
-
if (hid != null) hexLayerIndex[hid] = layer;
|
|
619
|
-
const triIds = feature.properties?.triIds?.length ? feature.properties.triIds.join(', ') : 'N/A';
|
|
620
|
-
layer.bindPopup(`Hex ID: ${hid}<br>Triangles: ${triIds}`);
|
|
621
|
-
}
|
|
622
|
-
}).addTo(map);
|
|
934
|
+
refreshGridLayers(hexFC, trianglesFC, { addToMap: true });
|
|
623
935
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
936
|
+
if (!browseMode) {
|
|
937
|
+
const BottomLeftPanel = L.Control.extend({
|
|
938
|
+
options: {
|
|
939
|
+
position: 'bottomleft'
|
|
940
|
+
},
|
|
941
|
+
onAdd(map) {
|
|
942
|
+
const div = L.DomUtil.create('div', 'map-bottom-left-control');
|
|
943
|
+
div.innerHTML = `
|
|
631
944
|
<div class="panel-contents">
|
|
632
945
|
<button class="btn"id="doSomething">Start Crawling!</button>
|
|
633
946
|
</div>`;
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
socket.emit("crawling", {
|
|
646
|
-
sessionId: sessionId,
|
|
647
|
-
});
|
|
648
|
-
}, {
|
|
649
|
-
once: true
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
return div;
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
myStartBtn = new BottomLeftPanel().addTo(map);
|
|
656
|
-
|
|
657
|
-
// 三角形
|
|
658
|
-
triLayer = L.geoJSON(trianglesFC, {
|
|
659
|
-
style: {
|
|
660
|
-
weight: 1,
|
|
661
|
-
opacity: 0.8,
|
|
662
|
-
fillOpacity: 0,
|
|
663
|
-
fillColor: "#EC407A",
|
|
664
|
-
color: "#F50057",
|
|
665
|
-
dashArray: "15,30",
|
|
666
|
-
dashOffset: 0,
|
|
667
|
-
},
|
|
668
|
-
onEachFeature: (feature, layer) => {
|
|
669
|
-
const pid = feature.properties?.parentHexId ?? 'N/A';
|
|
670
|
-
const tid = feature.properties?.triangleId ?? 'N/A';
|
|
671
|
-
const cross = feature.properties?.crossNeighbors ?? [];
|
|
672
|
-
const neighborHexIds = feature.properties?.neighborHexIds ?? [];
|
|
673
|
-
|
|
674
|
-
triLayerIndex[tid] = layer;
|
|
675
|
-
|
|
676
|
-
layer.bindPopup(
|
|
677
|
-
`Triangle: ${tid}<br>` +
|
|
678
|
-
`Parent Hex: ${pid}<br>` +
|
|
679
|
-
`Cross neighbors: ${cross.length ? cross.join(', ') : 'None'}<br>` +
|
|
680
|
-
`Neighbor Hexes: ${neighborHexIds.length ? neighborHexIds.join(', ') : 'None'}`
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
layer.on('click', () => {
|
|
684
|
-
// クリック時:当該三角形+交差隣接三角形をハイライト
|
|
685
|
-
clearHexHighlight();
|
|
686
|
-
clearTriHighlight();
|
|
687
|
-
highlightHexById(pid); // 親Hexも強調
|
|
688
|
-
highlightTriangle(tid, {
|
|
689
|
-
weight: 3,
|
|
690
|
-
opacity: 1,
|
|
691
|
-
fillOpacity: 0.30
|
|
692
|
-
}); // クリック対象
|
|
693
|
-
|
|
694
|
-
// 隣接(別Hex)の三角形を同時に強調
|
|
695
|
-
cross.forEach(cid => {
|
|
696
|
-
highlightTriangle(cid, {
|
|
697
|
-
weight: 2,
|
|
698
|
-
opacity: 1,
|
|
699
|
-
fillOpacity: 0.25
|
|
947
|
+
L.DomEvent.disableClickPropagation(div);
|
|
948
|
+
L.DomEvent.disableScrollPropagation(div);
|
|
949
|
+
|
|
950
|
+
const btn = div.querySelector('#doSomething');
|
|
951
|
+
btn.addEventListener('click', (ev) => {
|
|
952
|
+
ev.preventDefault();
|
|
953
|
+
ev.stopPropagation();
|
|
954
|
+
if (myStartBtn) map.removeControl(myStartBtn);
|
|
955
|
+
myProgress = new BottomLeftProgressBar().addTo(map);
|
|
956
|
+
socket.emit("crawling", {
|
|
957
|
+
sessionId: sessionId,
|
|
700
958
|
});
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (neighborLayer) {
|
|
704
|
-
const pHex = neighborLayer.feature?.properties?.parentHexId;
|
|
705
|
-
if (pHex != null) highlightHexById(pHex);
|
|
706
|
-
}
|
|
959
|
+
}, {
|
|
960
|
+
once: true
|
|
707
961
|
});
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
// マウスアウトで三角形のハイライトだけ戻す(Hexは維持)
|
|
711
|
-
layer.on('mouseout', () => {
|
|
712
|
-
clearTriHighlight();
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
});
|
|
716
962
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
963
|
+
return div;
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
myStartBtn = new BottomLeftPanel().addTo(map);
|
|
720
967
|
}
|
|
721
968
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
969
|
+
const layers = [hexLayer, triLayer].filter(Boolean);
|
|
970
|
+
if (layers.length > 0) {
|
|
971
|
+
const bounds = L.featureGroup(layers).getBounds();
|
|
972
|
+
if (bounds.isValid()) map.fitBounds(bounds, {
|
|
973
|
+
padding: [20, 20]
|
|
974
|
+
});
|
|
726
975
|
}
|
|
727
|
-
|
|
728
|
-
// ビュー調整
|
|
729
|
-
const layers = [hexLayer];
|
|
730
|
-
if (document.getElementById('toggleTriangles').checked) layers.push(triLayer);
|
|
731
|
-
const bounds = L.featureGroup(layers).getBounds();
|
|
732
|
-
if (bounds.isValid()) map.fitBounds(bounds, {
|
|
733
|
-
padding: [20, 20]
|
|
734
|
-
});
|
|
735
976
|
}
|
|
736
977
|
|
|
737
978
|
// API呼び出し
|
|
738
|
-
async function loadHexgridByBounds(bounds, drawn = null) {
|
|
979
|
+
async function loadHexgridByBounds(bounds, drawn = null, shapeKind = null) {
|
|
980
|
+
if (browseMode) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
currentBboxArray = [
|
|
984
|
+
Number(bounds.getWest().toFixed(6)),
|
|
985
|
+
Number(bounds.getSouth().toFixed(6)),
|
|
986
|
+
Number(bounds.getEast().toFixed(6)),
|
|
987
|
+
Number(bounds.getNorth().toFixed(6)),
|
|
988
|
+
];
|
|
989
|
+
currentDrawnGeoJSON = drawn || null;
|
|
990
|
+
currentDrawKind = drawn ? (shapeKind || 'polygon') : null;
|
|
739
991
|
const bbox = boundsToBbox(bounds);
|
|
740
992
|
document.getElementById('bboxLabel').textContent = `BBOX: ${bbox}`;
|
|
993
|
+
updateCliCommand();
|
|
741
994
|
|
|
742
995
|
const cellSize = document.getElementById('cellSize').value.trim();
|
|
743
996
|
const units = document.getElementById('units').value.trim();
|
|
744
997
|
const keywords = document.getElementById('keywords').value.trim();
|
|
745
|
-
|
|
746
|
-
latestResult["_"]["keywords"] = keywords;
|
|
998
|
+
resultExtras.keywords = keywords;
|
|
747
999
|
socket.once("hexgrid", (res) => {
|
|
748
1000
|
if (res.sessionId) sessionId = res.sessionId;
|
|
749
1001
|
//console.log(`hexgrid received (${res.hex.features.length} hexes, ${res.triangles.features.length} triangles)`);
|
|
1002
|
+
Object.keys(hexProgress).forEach((key) => delete hexProgress[key]);
|
|
750
1003
|
const hex = res.hex;
|
|
751
1004
|
const triangles = res.triangles;
|
|
752
|
-
|
|
753
|
-
|
|
1005
|
+
resultExtras.hex = hex;
|
|
1006
|
+
resultExtras.triangles = [triangles];
|
|
1007
|
+
renderStatsPanel(null);
|
|
754
1008
|
renderLayers(hex, triangles);
|
|
755
1009
|
});
|
|
756
1010
|
socket.emit("target", {
|
|
@@ -765,51 +1019,133 @@
|
|
|
765
1019
|
});
|
|
766
1020
|
}
|
|
767
1021
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
await loadHexgridByBounds(layer.getBounds(), layer.toGeoJSON());
|
|
775
|
-
} catch (err) {
|
|
776
|
-
alert(err.message);
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
map.on(L.Draw.Event.EDITSTOP, async () => {
|
|
781
|
-
const layers = drawnItems.getLayers();
|
|
782
|
-
if (layers.length === 1) {
|
|
1022
|
+
if (!browseMode) {
|
|
1023
|
+
map.on(L.Draw.Event.CREATED, async (e) => {
|
|
1024
|
+
drawnItems.clearLayers();
|
|
1025
|
+
const layer = e.layer;
|
|
1026
|
+
layer.__splatoneShape = e.layerType === 'rectangle' ? 'rectangle' : 'polygon';
|
|
1027
|
+
drawnItems.addLayer(layer);
|
|
783
1028
|
try {
|
|
784
|
-
await loadHexgridByBounds(
|
|
1029
|
+
await loadHexgridByBounds(layer.getBounds(), layer.toGeoJSON(), layer.__splatoneShape);
|
|
785
1030
|
} catch (err) {
|
|
786
1031
|
alert(err.message);
|
|
787
1032
|
}
|
|
788
|
-
}
|
|
789
|
-
});
|
|
1033
|
+
});
|
|
790
1034
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1035
|
+
map.on(L.Draw.Event.EDITSTOP, async () => {
|
|
1036
|
+
const layers = drawnItems.getLayers();
|
|
1037
|
+
if (layers.length === 1) {
|
|
1038
|
+
try {
|
|
1039
|
+
const layer = layers[0];
|
|
1040
|
+
const shapeKind = layer.__splatoneShape || (layer instanceof L.Rectangle ? 'rectangle' : 'polygon');
|
|
1041
|
+
await loadHexgridByBounds(layer.getBounds(), layer.toGeoJSON(), shapeKind);
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
alert(err.message);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
map.on(L.Draw.Event.DELETED, () => {
|
|
1049
|
+
drawnItems.clearLayers();
|
|
1050
|
+
if (hexLayer) {
|
|
1051
|
+
map.removeLayer(hexLayer);
|
|
1052
|
+
hexLayer = null;
|
|
1053
|
+
}
|
|
1054
|
+
if (triLayer) {
|
|
1055
|
+
map.removeLayer(triLayer);
|
|
1056
|
+
triLayer = null;
|
|
1057
|
+
}
|
|
1058
|
+
if (myStartBtn) {
|
|
1059
|
+
map.removeControl(myStartBtn);
|
|
1060
|
+
myStartBtn = null;
|
|
1061
|
+
}
|
|
1062
|
+
if (myLegend) {
|
|
1063
|
+
document.getElementById('map_legend')?.replaceChildren();
|
|
1064
|
+
map.removeControl(myLegend);
|
|
1065
|
+
myLegend = null;
|
|
1066
|
+
}
|
|
1067
|
+
document.getElementById('bboxLabel').textContent = '';
|
|
1068
|
+
currentDrawnGeoJSON = null;
|
|
1069
|
+
currentDrawKind = null;
|
|
1070
|
+
currentBboxArray = null;
|
|
1071
|
+
updateCliCommand();
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (browseMode) {
|
|
1076
|
+
initDropImport();
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function initDropImport() {
|
|
1080
|
+
const overlay = document.getElementById('dropOverlay');
|
|
1081
|
+
if (!overlay) return;
|
|
1082
|
+
let dragDepth = 0;
|
|
1083
|
+
const showOverlay = () => {
|
|
1084
|
+
overlay.hidden = false;
|
|
1085
|
+
overlay.classList.add('visible');
|
|
1086
|
+
};
|
|
1087
|
+
const hideOverlay = () => {
|
|
1088
|
+
overlay.classList.remove('visible');
|
|
1089
|
+
overlay.hidden = true;
|
|
1090
|
+
};
|
|
1091
|
+
const hasFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes('Files');
|
|
1092
|
+
|
|
1093
|
+
window.addEventListener('dragenter', (event) => {
|
|
1094
|
+
if (!hasFiles(event)) return;
|
|
1095
|
+
dragDepth += 1;
|
|
1096
|
+
showOverlay();
|
|
1097
|
+
event.preventDefault();
|
|
1098
|
+
});
|
|
1099
|
+
window.addEventListener('dragover', (event) => {
|
|
1100
|
+
if (!hasFiles(event)) return;
|
|
1101
|
+
event.preventDefault();
|
|
1102
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
1103
|
+
});
|
|
1104
|
+
window.addEventListener('dragleave', (event) => {
|
|
1105
|
+
if (dragDepth > 0) {
|
|
1106
|
+
dragDepth -= 1;
|
|
1107
|
+
}
|
|
1108
|
+
if (dragDepth <= 0) {
|
|
1109
|
+
dragDepth = 0;
|
|
1110
|
+
hideOverlay();
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
window.addEventListener('drop', async (event) => {
|
|
1114
|
+
if (!hasFiles(event)) return;
|
|
1115
|
+
event.preventDefault();
|
|
1116
|
+
dragDepth = 0;
|
|
1117
|
+
hideOverlay();
|
|
1118
|
+
const file = event.dataTransfer?.files?.[0] ?? null;
|
|
1119
|
+
if (file) {
|
|
1120
|
+
await importResultFile(file);
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
async function importResultFile(file) {
|
|
1126
|
+
try {
|
|
1127
|
+
const text = await file.text();
|
|
1128
|
+
let parsed;
|
|
1129
|
+
try {
|
|
1130
|
+
parsed = JSON.parse(text);
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
throw new Error('JSONとして解析できませんでした');
|
|
1133
|
+
}
|
|
1134
|
+
const bundle = parsed?.bundle ?? parsed;
|
|
1135
|
+
if (!bundle || typeof bundle !== 'object' || !bundle.geoJson) {
|
|
1136
|
+
throw new Error('結果バンドルではありません');
|
|
1137
|
+
}
|
|
1138
|
+
await visualizeBundle(bundle, { sourceLabel: file.name || 'file' });
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
Toastify({
|
|
1141
|
+
text: `読み込みに失敗しました: ${err?.message || err}`,
|
|
1142
|
+
duration: 4000,
|
|
1143
|
+
gravity: 'bottom',
|
|
1144
|
+
position: 'center',
|
|
1145
|
+
className: 'toast-error'
|
|
1146
|
+
}).showToast();
|
|
809
1147
|
}
|
|
810
|
-
|
|
811
|
-
document.getElementById('bboxLabel').textContent = '';
|
|
812
|
-
});
|
|
1148
|
+
}
|
|
813
1149
|
|
|
814
1150
|
// ======= ハンバーガー / パネル制御 =======
|
|
815
1151
|
const hamburger = document.getElementById('hamburger');
|
|
@@ -843,46 +1179,53 @@
|
|
|
843
1179
|
if (e.key === 'Escape') closePanel();
|
|
844
1180
|
});
|
|
845
1181
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if (hexLayer) {
|
|
850
|
-
map.removeLayer(hexLayer);
|
|
851
|
-
hexLayer = null;
|
|
852
|
-
}
|
|
853
|
-
if (triLayer) {
|
|
854
|
-
map.removeLayer(triLayer);
|
|
855
|
-
triLayer = null;
|
|
856
|
-
}
|
|
857
|
-
if (myStartBtn) {
|
|
858
|
-
map.removeControl(myStartBtn);
|
|
859
|
-
myStartBtn = null;
|
|
1182
|
+
async function applyDefaultGeometry() {
|
|
1183
|
+
if (!defaultGeometry || (!defaultGeometry.polygon && !Array.isArray(defaultGeometry.bbox))) {
|
|
1184
|
+
return;
|
|
860
1185
|
}
|
|
861
|
-
if (myLegend) {
|
|
862
|
-
document.getElementById('map_legend')?.replaceChildren();
|
|
863
|
-
map.removeControl(myLegend);
|
|
864
|
-
myLegend = null;
|
|
865
|
-
}
|
|
866
|
-
hexLabelLayer.removeFrom(map);
|
|
867
|
-
document.getElementById('bboxLabel').textContent = '';
|
|
868
|
-
});
|
|
869
1186
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1187
|
+
const ensureDraw = async (layer) => {
|
|
1188
|
+
await welcomeReady;
|
|
1189
|
+
drawnItems.clearLayers();
|
|
1190
|
+
layer.__splatoneShape = layer.__splatoneShape || (layer instanceof L.Rectangle ? 'rectangle' : 'polygon');
|
|
1191
|
+
drawnItems.addLayer(layer);
|
|
1192
|
+
await loadHexgridByBounds(layer.getBounds(), layer.toGeoJSON(), layer.__splatoneShape);
|
|
1193
|
+
};
|
|
875
1194
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1195
|
+
if (defaultGeometry.polygon && typeof defaultGeometry.polygon === 'object') {
|
|
1196
|
+
const geoLayer = L.geoJSON(defaultGeometry.polygon);
|
|
1197
|
+
const layers = geoLayer.getLayers();
|
|
1198
|
+
if (layers.length > 0) {
|
|
1199
|
+
const layer = layers[0];
|
|
1200
|
+
if (layer.getBounds().isValid()) {
|
|
1201
|
+
map.fitBounds(layer.getBounds(), { padding: [20, 20] });
|
|
1202
|
+
}
|
|
1203
|
+
layer.__splatoneShape = 'polygon';
|
|
1204
|
+
await ensureDraw(layer);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
881
1207
|
}
|
|
882
|
-
});
|
|
883
1208
|
|
|
884
|
-
|
|
885
|
-
|
|
1209
|
+
if (Array.isArray(defaultGeometry.bbox) && defaultGeometry.bbox.length === 4) {
|
|
1210
|
+
const [minLon, minLat, maxLon, maxLat] = defaultGeometry.bbox;
|
|
1211
|
+
if ([minLon, minLat, maxLon, maxLat].every(val => Number.isFinite(Number(val)))) {
|
|
1212
|
+
const bounds = L.latLngBounds([
|
|
1213
|
+
[minLat, minLon],
|
|
1214
|
+
[maxLat, maxLon]
|
|
1215
|
+
]);
|
|
1216
|
+
const rectangle = L.rectangle(bounds, { weight: 1, fillOpacity: 0 });
|
|
1217
|
+
rectangle.__splatoneShape = 'rectangle';
|
|
1218
|
+
if (bounds.isValid()) {
|
|
1219
|
+
map.fitBounds(bounds, { padding: [20, 20] });
|
|
1220
|
+
}
|
|
1221
|
+
await ensureDraw(rectangle);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (!browseMode && (defaultGeometry?.polygon || Array.isArray(defaultGeometry?.bbox))) {
|
|
1227
|
+
applyDefaultGeometry().catch((err) => console.error('Failed to apply default geometry', err));
|
|
1228
|
+
}
|
|
886
1229
|
</script>
|
|
887
1230
|
</body>
|
|
888
1231
|
|