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 +12 -9
- package/assets/screenshot_massive_points_bulky.png +0 -0
- package/crawler.js +90 -52
- package/lib/splatone.js +22 -6
- package/package.json +2 -1
- package/public/visualizer.js +74 -1
- package/views/index.ejs +47 -17
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
|
-
|
|
17
|
-
- サイズの問題は一部解決しました。ただし、**データの保存(ダウンロード)はまだできません。**
|
|
18
|
-
- 問題はサーバからブラウザへ結果を渡す時にJSON.stringify(obj)で結果データをJSON文字列にする時にヒープを食いつぶしています。 とりあえず可視化だけ出来るように、データが大きいと自動で分割して送信するようにしています。ただし、送受信に時間がかかります。(左下のプログレスバーに進捗が表示されます)
|
|
19
|
-
- **--chopped**オプションをつけると強制的に分割送信します。(ただ遅くなるだけなので意味ないです)
|
|
20
|
-
- データ保存(ダウンロード)する際にブラウザ上でJSON.stringify(json)を呼ぶ必要があり、そこで詰まります。これの解決策は別途実装します。
|
|
16
|
+
### v0.0.6 → v0.0.7
|
|
21
17
|
|
|
22
|
-
|
|
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
|
-
-
|
|
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
|

|
|
164
162
|
|
|
163
|
+
### 広範囲なデータ収集例
|
|
164
|
+
|
|
165
|
+
* あまりにも大きいとFlickrから一時的にBANされることがありますので注意してください。
|
|
166
|
+
|
|
167
|
+

|
|
Binary file
|
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 --
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
645
|
-
|
|
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
|
-
}
|
|
663
|
-
|
|
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
|
-
|
|
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
|
|
21
|
-
const east
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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",
|
package/public/visualizer.js
CHANGED
|
@@ -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
|
|
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({
|
|
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
|
-
|
|
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({
|
|
389
|
+
callback({
|
|
390
|
+
pong: Date.now(),
|
|
391
|
+
ok: true
|
|
392
|
+
});
|
|
363
393
|
});
|
|
364
394
|
|
|
365
395
|
// 地図
|