splatone 0.0.5 → 0.0.7

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/README.md CHANGED
@@ -11,15 +11,12 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
11
11
  - Bulky: クロールした全てのジオタグを小さな点で描画する
12
12
  - Marker Cluster: 密集しているジオタグをクラスタリングしてまとめて表示する
13
13
 
14
- ## 既知のバグ
14
+ ## Change Log
15
15
 
16
- - <s>JSON.stringify(json)で変換できる大きさに制限があり、数十万件等の大きな結果を生み出すクエリは、クロール後、結果のブラウザへの転送で失敗します。</s>
17
- - サイズの問題は一部解決しました。ただし、**データの保存(ダウンロード)はまだできません。**
18
- - 問題はサーバからブラウザへ結果を渡す時にJSON.stringify(obj)で結果データをJSON文字列にする時にヒープを食いつぶしています。 とりあえず可視化だけ出来るように、データが大きいと自動で分割して送信するようにしています。ただし、送受信に時間がかかります。(左下のプログレスバーに進捗が表示されます)
19
- - **--chopped**オプションをつけると強制的に分割送信します。(ただ遅くなるだけなので意味ないです)
20
- - データ保存(ダウンロード)する際にブラウザ上でJSON.stringify(json)を呼ぶ必要があり、そこで詰まります。これの解決策は別途実装します。
16
+ ### v0.0.6 → v0.0.7
21
17
 
22
- ![](/assets/screenshot_massive_points_bulky.png)
18
+ * Hexサイズの自動設定モードが実装され、デフォルトとなりました。
19
+ * Web画面のハンバーガーメニューから変更できます。(サイズ0で自動)
23
20
 
24
21
  # 使い方
25
22
 
@@ -31,14 +28,15 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
31
28
  ```shell
32
29
  $ npx -y -- splatone@latest crawler --help
33
30
  使い方: crawler.js [options]
34
-
35
31
  Basic Options
36
32
  -p, --plugin 実行するプラグイン[文字列] [必須] [選択してください: "flickr"]
37
33
  -o, --options プラグインオプション [文字列] [デフォルト: "{}"]
38
34
  -k, --keywords 検索キーワード(|区切り) [文字列] [デフォルト:
39
35
  "nature,tree,flower|building,house|water,sea,river,pond"]
40
- -c, --chopped 大きなデータを細分化して送信する
36
+ -f, --filed 大きなデータをファイルとして送受信する
41
37
  [真偽] [デフォルト: true]
38
+ -c, --chopped 大きなデータを細分化して送受信する
39
+ [非推奨] [真偽] [デフォルト: false]
42
40
 
43
41
  Debug
44
42
  --debug-verbose デバッグ情報出力 [真偽] [デフォルト: false]
@@ -162,3 +160,8 @@ $ node crawler.js -p flickr -o '{"flickr":{"API_KEY":"aaaaaaaaaaaaaaaaaaaaaaaaaa
162
160
 
163
161
  ![](https://github.com/YokoyamaLab/Splatone/blob/main/assets/icon_data_export.png?raw=true)
164
162
 
163
+ ### 広範囲なデータ収集例
164
+
165
+ * あまりにも大きいとFlickrから一時的にBANされることがありますので注意してください。
166
+
167
+ ![](/assets/screenshot_massive_points_bulky.png)
package/crawler.js CHANGED
@@ -28,7 +28,7 @@ import { hideBin } from 'yargs/helpers';
28
28
  // -------------------------------
29
29
  import { loadPlugins } from './lib/pluginLoader.js';
30
30
  import paletteGenerator from './lib/paletteGenerator.js';
31
- import { dfsObject } from './lib/splatone.js';
31
+ import { dfsObject, bboxSize, saveGeoJsonObjectAsStream } from './lib/splatone.js';
32
32
 
33
33
  const __filename = fileURLToPath(import.meta.url);
34
34
  const __dirname = dirname(__filename);
@@ -108,7 +108,7 @@ try {
108
108
  // コマンド例
109
109
  // node crawler.js -p flickr -o '{"flickr":{"API_KEY":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}' -k "商業=shop,souvenir,market,supermarket,pharmacy,store,department|食べ物=food,drink,restaurant,cafe,bar|美術 館=museum,art,exhibition,expo,sculpture,heritage|公園=park,garden,flower,green,pond,playground" --vis-bulky
110
110
  // node crawler.js -p flickr -k "水域=canal,channel,waterway,river,stream,watercourse,sea,ocean,gulf,bay,strait,lagoon,offshore|橋梁=bridge,overpass,flyover,aqueduct,trestle|通路=street,road,thoroughfare,roadway,avenue,boulevard,lane,alley,roadway,carriageway,highway,motorway|ランドマーク=church,sanctuary,chapel,cathedral,basilica,minster,abbey,temple,shrine" --vis-bulky
111
- // node crawler.js -p flickr -k "水辺=sea,ocean,beach|山岳=mountain,mount,hill" --vis-bulky --chopped
111
+ // node crawler.js -p flickr -k "水辺=sea,ocean,beach|山岳=mountain,mount,hill" --vis-bulky --filed
112
112
  let yargv = await yargs(hideBin(process.argv))
113
113
  .strict() // 未定義オプションはエラー
114
114
  .usage('使い方: $0 [options]')
@@ -133,12 +133,19 @@ try {
133
133
  type: 'string',
134
134
  default: 'nature,tree,flower|building,house|water,sea,river,pond',
135
135
  description: '検索キーワード(|区切り)'
136
+ }).option('filed', {
137
+ group: 'Basic Options',
138
+ alias: 'f',
139
+ type: 'boolean',
140
+ default: true,
141
+ description: '大きなデータをファイルとして送受信する'
136
142
  }).option('chopped', {
137
143
  group: 'Basic Options',
138
144
  alias: 'c',
139
145
  type: 'boolean',
140
146
  default: false,
141
- description: '大きなデータを細分化して送信する'
147
+ deprecate: true,
148
+ description: '大きなデータを細分化して送受信する'
142
149
  // }).option('debug-save', {
143
150
  // group: 'Debug',
144
151
  // type: 'boolean',
@@ -169,6 +176,10 @@ try {
169
176
  if (Object.keys(all_visualizers).filter(v => argv["vis-" + v]).length == 0) {
170
177
  throw new Error('可視化ツールの指定がありません。最低一つは指定してください。');
171
178
  }
179
+ if (argv.filed && argv.chopped) {
180
+ console.warn("--filedと--choppedが両方指定されています。--filedが優先されます。");
181
+ argv.chopped = false;
182
+ }
172
183
  return true;
173
184
  });
174
185
  const argv = await yargv.parseAsync();
@@ -293,7 +304,7 @@ try {
293
304
  title: title,
294
305
  lat: DEFAULT_CENTER.lat,
295
306
  lon: DEFAULT_CENTER.lon,
296
- defaultCellSize: 0.5,
307
+ defaultCellSize: 0,
297
308
  defaultUnits: 'kilometers',
298
309
  defaultKeywords: argv.keywords,
299
310
  });
@@ -346,19 +357,32 @@ try {
346
357
  console.warn("invalid sessionId:", req.sessionId);
347
358
  return;
348
359
  }
349
- const { bbox, drawn, cellSize = 0, units = 'kilometers', tags = 'sea,beach|mountain,forest' } = req.query;
360
+ let { bbox, drawn, cellSize = 0, units = 'kilometers', tags = 'sea,beach|mountain,forest' } = req.query;
361
+ const boundary = String(bbox).split(',').map(Number);
350
362
  if (cellSize == 0) {
351
363
  //セルサイズ自動決定()
364
+ //console.log("[cellSize?]",boundary,units);
365
+ const { width, height } = bboxSize(boundary, units);
366
+ //console.log("","w=",width,"/\th=",height);
367
+ cellSize = Math.max(width / (3 * 30), height / (30 * Math.sqrt(3)));
368
+ if(cellSize==0){
369
+ cellSize=1;
370
+ }
371
+ const msg = "セルサイズを[ " + cellSize + ' ' + units + " ]に設定しました。";
372
+ console.log(msg)
373
+ io.to(sessionId).timeout(5000).emit('toast', {
374
+ text: msg,
375
+ class: "info"
376
+ });
352
377
  }
353
378
  const fallbackBbox = [139.55, 35.53, 139.92, 35.80];
354
379
  let bboxArray = fallbackBbox;
355
380
 
356
381
  if (bbox) {
357
- const parts = String(bbox).split(',').map(Number);
358
- if (parts.length !== 4 || !parts.every(Number.isFinite)) {
382
+ if (boundary.length !== 4 || !boundary.every(Number.isFinite)) {
359
383
  return res.status(400).json({ error: 'bbox must be "minLon,minLat,maxLon,maxLat"' });
360
384
  }
361
- bboxArray = parts;
385
+ bboxArray = boundary;
362
386
  }
363
387
 
364
388
  const sizeNum = Number(cellSize);
@@ -601,10 +625,10 @@ try {
601
625
 
602
626
  console.log('[splatone:finish]');
603
627
  try {
604
- if (argv.chopped) {
628
+ if (argv.chopped || argv.filed) {
605
629
  throw new RangeError("Invalid string length");
606
630
  }
607
- await io.to(p.sessionId).timeout(5000).emitWithAck('result', {
631
+ await io.to(p.sessionId).timeout(120000).emitWithAck('result', {
608
632
  resultId,
609
633
  geoJson,
610
634
  palette: target["splatonePalette"],
@@ -613,7 +637,7 @@ try {
613
637
  });
614
638
  } catch (e) {
615
639
  if (e instanceof RangeError && /Invalid string length/.test(String(e.message))) {
616
- const msg = (argv.chopped ? "ユーザの指定により" : "結果サイズが巨大なので") + "断片化モードでクライアントに送ります";
640
+ const msg = ((argv.chopped || argv.filed) ? "ユーザの指定により" : "結果サイズが巨大なので") + (argv.chopped ? "断片化送信" : "保存ファイル送信") + "モードでクライアントに送ります";
617
641
  if (argv.debugVerbose) {
618
642
  console.warn("[WARN] " + msg);
619
643
  }
@@ -621,50 +645,64 @@ try {
621
645
  text: msg,
622
646
  class: "warning"
623
647
  });
624
- //サイズ集計
625
- const total_features = ((s = 0, st = [geoJson], v, seen = new WeakSet) => { for (; st.length;)if ((v = st.pop()) && typeof v === 'object' && !seen.has(v)) { seen.add(v); if (Array.isArray(v.features)) s += v.features.length; for (const k in v) { const x = v[k]; if (x && typeof x === 'object') st.push(x) } } return s })();
626
- let current_features = 0
627
- await dfsObject(geoJson, async ({ path, value, kind, type }) => {
628
- if (path.length !== 0) {
629
- if (kind === "primitive" || kind === "null") {
630
- //console.log(path.join("."), "=>", `(${kind}:${type})`, value);
631
- const ackrtn = await io.to(p.sessionId).timeout(5000).emitWithAck('result-chunk', {
632
- resultId,
633
- path,
634
- kind,
635
- type,
636
- value
637
- });
638
- //console.log("\tACK", ackrtn);
639
- } else if (kind === "object") {
640
- //console.log(path.join("."), "=>", `(${kind}:${type})`);
641
- if (path.at(-2) == "features" && Number.isInteger(path.at(-1))) {
642
- current_features++;
648
+ if (argv.chopped) {
649
+ //サイズ集計
650
+ const total_features = ((s = 0, st = [geoJson], v, seen = new WeakSet) => { for (; st.length;)if ((v = st.pop()) && typeof v === 'object' && !seen.has(v)) { seen.add(v); if (Array.isArray(v.features)) s += v.features.length; for (const k in v) { const x = v[k]; if (x && typeof x === 'object') st.push(x) } } return s })();
651
+ let current_features = 0
652
+ await dfsObject(geoJson, async ({ path, value, kind, type }) => {
653
+ if (path.length !== 0) {
654
+ if (kind === "primitive" || kind === "null") {
655
+ //console.log(path.join("."), "=>", `(${kind}:${type})`, value);
656
+ const ackrtn = await io.to(p.sessionId).timeout(120000).emitWithAck('result-chunk', {
657
+ resultId,
658
+ path,
659
+ kind,
660
+ type,
661
+ value
662
+ });
663
+ //console.log("\tACK", ackrtn);
664
+ } else if (kind === "object") {
665
+ //console.log(path.join("."), "=>", `(${kind}:${type})`);
666
+ if (path.at(-2) == "features" && Number.isInteger(path.at(-1))) {
667
+ current_features++;
668
+ }
669
+ const ackrtn = await io.to(p.sessionId).timeout(120000).emitWithAck('result-chunk', {
670
+ resultId,
671
+ path,
672
+ kind,
673
+ type,
674
+ progress: { current: current_features, total: total_features }
675
+ });
676
+ //console.log("\tACK", ackrtn);
677
+ } else if (kind === "array") {
678
+ //console.log(path.join("."), "=>", `(${kind}:${type})`);
679
+ const ackrtn = await io.to(p.sessionId).timeout(120000).emitWithAck('result-chunk', {
680
+ resultId,
681
+ path,
682
+ kind,
683
+ type
684
+ });
685
+ //console.log("\tACK", ackrtn);
643
686
  }
644
- const ackrtn = await io.to(p.sessionId).timeout(5000).emitWithAck('result-chunk', {
645
- resultId,
646
- path,
647
- kind,
648
- type,
649
- progress: { current: current_features, total: total_features }
650
- });
651
- //console.log("\tACK", ackrtn);
652
- } else if (kind === "array") {
653
- //console.log(path.join("."), "=>", `(${kind}:${type})`);
654
- const ackrtn = await io.to(p.sessionId).timeout(5000).emitWithAck('result-chunk', {
655
- resultId,
656
- path,
657
- kind,
658
- type
659
- });
660
- //console.log("\tACK", ackrtn);
687
+ } else {
688
+ //console.log("SKIP---------------------");
661
689
  }
662
- } else {
663
- //console.log("SKIP---------------------");
690
+ });
691
+ //console.log("finish chunks");
692
+ } else {
693
+ //保存ファイル送信(--filed)
694
+ try {
695
+ const outPath = await saveGeoJsonObjectAsStream(geoJson, 'result.' + resultId + '.json');
696
+ console.log('saved:', outPath);
697
+ const ackrtn = await io.to(p.sessionId).timeout(120000).emitWithAck('result-file', {
698
+ resultId,
699
+ });
700
+ } catch (err) {
701
+ console.error('failed:', err);
702
+ process.exitCode = 1;
664
703
  }
665
- });
666
- //console.log("finish chunks");
667
- io.to(p.sessionId).timeout(10000).emitWithAck('result', {
704
+ }
705
+ await io.to(p.sessionId).timeout(120000).emitWithAck('result', {
668
706
  resultId,
669
707
  geoJson: null, /*geoJsonは送らない*/
670
708
  palette: target["splatonePalette"],
package/lib/splatone.js CHANGED
@@ -1,6 +1,12 @@
1
1
  // 各種関数のまとめ場
2
2
  // @turf/turf v6/v7 どちらでもOK(rhumbDistanceはv7で統合済み)
3
3
  import { point, distance, rhumbDistance, bbox as turfBbox } from '@turf/turf';
4
+ import { createWriteStream } from 'node:fs';
5
+ import { mkdir } from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { pipeline as pipelineCb } from 'node:stream';
8
+ import { promisify } from 'node:util';
9
+ import { JsonStreamStringify } from 'json-stream-stringify';
4
10
 
5
11
  /**
6
12
  * bbox の幅・高さを指定単位で返す
@@ -17,8 +23,8 @@ export function bboxSize(bbox, units = "kilometers", method = "geodesic") {
17
23
  const midLon = (minX + maxX) / 2;
18
24
 
19
25
  // 測りたい2点を作成
20
- const west = point([minX, midLat]);
21
- const east = point([maxX, midLat]);
26
+ const west = point([minX, midLat]);
27
+ const east = point([maxX, midLat]);
22
28
  const south = point([midLon, minY]);
23
29
  const north = point([midLon, maxY]);
24
30
 
@@ -26,7 +32,7 @@ export function bboxSize(bbox, units = "kilometers", method = "geodesic") {
26
32
  const distFn = method === "rhumb" ? rhumbDistance : distance;
27
33
 
28
34
  return {
29
- width: distFn(west, east, { units }),
35
+ width: distFn(west, east, { units }),
30
36
  height: distFn(south, north, { units }),
31
37
  units
32
38
  };
@@ -127,9 +133,19 @@ function describeType(value) {
127
133
  const ctor = value?.constructor?.name ?? "Object";
128
134
  return { kind: "object", type: ctor, isLeaf: false, isNull: false };
129
135
  }
130
-
131
-
136
+ const pipeline = promisify(pipelineCb);
137
+ export async function saveGeoJsonObjectAsStream(geoJsonObject, outfile) {
138
+ const dir = path.join('public', 'out');
139
+ await mkdir(dir, { recursive: true });
140
+ const destPath = path.join(dir, path.basename(outfile));
141
+
142
+ // ここでも JSON.stringify は呼ばず、ストリームで直に文字列化
143
+ const src = new JsonStreamStringify(geoJsonObject);
144
+ const dest = createWriteStream(destPath, { flags: 'w' });
145
+ await pipeline(src, dest);
146
+ return destPath;
147
+ }
132
148
  export default {
133
- dfsObject,bboxSize
149
+ dfsObject, bboxSize, saveGeoJsonObjectAsStream
134
150
  };
135
151
  //const {width,height} = bboxSize(b, "kilometers", "geodesic");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "splatone",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Multi-layer Composite Heatmap",
5
5
  "homepage": "https://github.com/YokoyamaLab/Splatone#readme",
6
6
  "bugs": {
@@ -27,6 +27,7 @@
27
27
  "express": "^5.1.0",
28
28
  "flickr-sdk": "^7.1.0",
29
29
  "iwanthue": "^2.0.0",
30
+ "json-stream-stringify": "^3.1.6",
30
31
  "open": "^10.2.0",
31
32
  "piscina": "^5.1.3",
32
33
  "socket.io": "^4.8.1",
@@ -61,4 +61,77 @@ function loadAsset(url) {
61
61
  }
62
62
 
63
63
  return Promise.reject(new Error(`Unsupported extension: ${url}`));
64
- }
64
+ }
65
+
66
+ async function fetchJsonObject(url, { signal } = {}) {
67
+ const res = await fetch(url, {
68
+ method: 'GET',
69
+ headers: { 'Accept': 'application/json' },
70
+ // same-originのときは不要。CORS越えならサーバ側のCORS許可が必要
71
+ signal,
72
+ cache: 'no-cache', // 必要なら更新を強制
73
+ });
74
+
75
+ if (!res.ok) {
76
+ // ステータスコード付きで例外
77
+ throw new Error(`HTTP ${res.status} ${res.statusText} for ${url}`);
78
+ }
79
+
80
+ // 自動で JSON parse され、JSオブジェクトになる
81
+ return await res.json();
82
+ }
83
+
84
+ /**
85
+ * 指定のサーバ上パス (filePath) から JSON を取得し、
86
+ * ブラウザで downloadFileName という名前でダウンロードさせる。
87
+ *
88
+ * @param {string} downloadFileName - 例: "data.geojson" / "result.json"
89
+ * @param {string} filePath - 例: "/public/out/data.geojson" or "https://example.com/data.json"
90
+ * @param {object} [opts]
91
+ * @param {number} [opts.timeoutMs=15000] - タイムアウト(ms)
92
+ * @param {RequestInit} [opts.fetchInit] - fetch の追加オプション(ヘッダ/credentials等)
93
+ */
94
+ async function downloadJSONFile(downloadFileName, filePath, opts = {}) {
95
+ const { timeoutMs = 15000, fetchInit = {} } = opts;
96
+
97
+ // タイムアウト制御
98
+ const ac = new AbortController();
99
+ const timer = setTimeout(() => ac.abort(new Error('timeout')), timeoutMs);
100
+
101
+ try {
102
+ const res = await fetch(filePath, {
103
+ method: 'GET',
104
+ headers: { 'Accept': 'application/json', ...(fetchInit.headers || {}) },
105
+ signal: ac.signal,
106
+ // CORS が必要なら credentials / mode などを fetchInit で渡す(例: { credentials: 'include' })
107
+ ...fetchInit,
108
+ });
109
+
110
+ if (!res.ok) {
111
+ throw new Error(`HTTP ${res.status} ${res.statusText} for ${filePath}`);
112
+ }
113
+
114
+ // そのまま Blob 化(JSON で返ってくる想定)
115
+ // MIME が不正のときもこちらで application/json に寄せる
116
+ const blob = await res.blob();
117
+ const type = res.headers.get('content-type');
118
+ const jsonBlob = type && type.includes('application/json')
119
+ ? blob
120
+ : new Blob([await blob.arrayBuffer()], { type: 'application/json;charset=utf-8' });
121
+
122
+ const url = URL.createObjectURL(jsonBlob);
123
+ try {
124
+ const a = document.createElement('a');
125
+ a.href = url;
126
+ a.download = downloadFileName; // ダウンロード名を強制
127
+ document.body.appendChild(a);
128
+ a.click();
129
+ a.remove();
130
+ } finally {
131
+ // 生成したオブジェクトURLは必ず破棄
132
+ URL.revokeObjectURL(url);
133
+ }
134
+ } finally {
135
+ clearTimeout(timer);
136
+ }
137
+ }
package/views/index.ejs CHANGED
@@ -151,6 +151,7 @@
151
151
  let sessionId = null;
152
152
  let visualizers = {};
153
153
  let results = {};
154
+ let files = {};
154
155
  let layerControl = null;
155
156
  socket.on("welcome", async (res) => {
156
157
  //console.log(`welcome ${res.sessionId} at ${new Date(res.time).toLocaleTimeString()}`);
@@ -251,19 +252,37 @@
251
252
  }
252
253
  socket.on("toast", (arg) => {
253
254
  const option = {
254
- text:"",
255
- duration:arg.close?-1:5000,
256
- close:false,
257
- gravity:'bottom',
258
- position:'center',
259
- className:"toast-" + arg.class,
260
- ...arg};
255
+ text: "",
256
+ duration: arg.close ? -1 : 5000,
257
+ close: false,
258
+ gravity: 'bottom',
259
+ position: 'center',
260
+ className: "toast-" + arg.class,
261
+ ...arg
262
+ };
261
263
  Toastify(option).showToast();
262
264
  //https://apvarun.github.io/toastify-js/#
263
265
  });
266
+ socket.on("result-file", async(arg, callback) => {
267
+ try {
268
+ const file_path = '/out/result.' + arg.resultId + '.json';
269
+ latestFile = file_path;
270
+ files[arg.resultId] = file_path;
271
+ console.log("file path",file_path);
272
+ results[arg.resultId] = await fetchJsonObject(file_path);
273
+ console.log('結果ファイル取得完了');
274
+ } catch (e) {
275
+ console.error('結果ファイル取得失敗:', e);
276
+ }
277
+ //console.log("ARK");
278
+ callback({
279
+ pong: Date.now(),
280
+ ok: true
281
+ }); // only one argument is expected
282
+ });
264
283
  socket.on("result-chunk", (arg, callback) => {
265
- if(arg.progress){
266
- setProgress(document.getElementById("progressCrawl"),Math.round((arg.progress.current/arg.progress.total) * 100000) / 1000);
284
+ if (arg.progress) {
285
+ setProgress(document.getElementById("progressCrawl"), Math.round((arg.progress.current / arg.progress.total) * 100000) / 1000);
267
286
  }
268
287
  results[arg.resultId] ??= {};
269
288
  if (arg.kind === "primitive" || arg.kind === "null") {
@@ -293,19 +312,23 @@
293
312
  setAt(results[arg.resultId], arg.path, []);
294
313
  }
295
314
  //console.log("ARK");
296
- callback({ pong: Date.now(), ok: true }); // only one argument is expected
315
+ callback({
316
+ pong: Date.now(),
317
+ ok: true
318
+ }); // only one argument is expected
297
319
  });
298
320
 
299
321
  let latestResult = {};
322
+ let latestFile = null;
300
323
  const overlays = {};
301
- socket.on('result', async (res,callback) => {
324
+ socket.on('result', async (res, callback) => {
302
325
  //console.log("result");
303
326
  latestResult["_"]["visualizers"] = res.visualizers;
304
327
  latestResult["_"]["plugin"] = res.plugin;
305
328
  clearHexHighlight();
306
- if(res.geoJson == null){
329
+ if (res.geoJson == null) {
307
330
  res.geoJson = results[res.resultId];
308
- }else{
331
+ } else {
309
332
  results[res.resultId] = res.geoJson;
310
333
  }
311
334
  latestResult = {
@@ -324,7 +347,7 @@
324
347
  overlays[`[${vis}]`] = layers;
325
348
  } else {
326
349
  for (const name in layers) {
327
- console.log("Lay",vis,name);
350
+ console.log("Lay", vis, name);
328
351
  overlays[`[${vis}] ${name}`] = layers[name];
329
352
  }
330
353
  }
@@ -354,12 +377,19 @@
354
377
  });
355
378
  layerControl.addTo(map);
356
379
  document.getElementById("map_legend")?.appendChild(ul);
357
- document.getElementById("download-json").addEventListener("click", () => {
380
+ document.getElementById("download-json").addEventListener("click", async () => {
358
381
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
359
- downloadJSON(`data-${stamp}.json`, latestResult);
382
+ if(latestFile===null){
383
+ downloadJSON(`splatone-${stamp}.json`, latestResult);
384
+ }else{
385
+ await downloadJSONFile(`splatone-${stamp}.json`, latestFile);
386
+ }
360
387
  let sessionId = null;
361
388
  });
362
- callback({ pong: Date.now(), ok: true });
389
+ callback({
390
+ pong: Date.now(),
391
+ ok: true
392
+ });
363
393
  });
364
394
 
365
395
  // 地図