splatone 0.0.4 → 0.0.6
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 +17 -7
- package/assets/screenshot_massive_points_bulky.png +0 -0
- package/crawler.js +143 -17
- package/lib/paletteGenerator.js +1 -1
- package/lib/splatone.js +151 -0
- package/package.json +2 -1
- package/plugins/flickr/worker.js +5 -4
- package/public/style.css +11 -1
- package/public/visualizer.js +89 -1
- package/views/index.ejs +131 -36
- package/visualizer/bulky/node.js +0 -1
- package/visualizer/bulky/web.js +2 -0
package/README.md
CHANGED
|
@@ -13,8 +13,13 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
|
|
|
13
13
|
|
|
14
14
|
## 既知のバグ
|
|
15
15
|
|
|
16
|
-
- JSON.stringify(json)
|
|
17
|
-
|
|
16
|
+
- <s>JSON.stringify(json)で変換できる大きさに制限があり、数十万件等の大きな結果を生み出すクエリは、クロール後、結果のブラウザへの転送で失敗します。</s>
|
|
17
|
+
- サイズの問題は、一度ファイルに書き出す事で解決しました。(v0.0.6)
|
|
18
|
+
- **--no-filed**オプションを付ける事で、従来の方法で実行できます。(ファイルに書き出すより多少速い)
|
|
19
|
+
- 以下の画像のように大きな範囲で多くのジオタグが収集できるようになりました。
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
18
23
|
# 使い方
|
|
19
24
|
|
|
20
25
|
- [Node.js](https://nodejs.org/ja/download)をインストール後、npxで実行します。
|
|
@@ -25,13 +30,18 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
|
|
|
25
30
|
```shell
|
|
26
31
|
$ npx -y -- splatone@latest crawler --help
|
|
27
32
|
使い方: crawler.js [options]
|
|
28
|
-
|
|
29
33
|
Basic Options
|
|
30
|
-
-p, --plugin 実行するプラグイン
|
|
31
|
-
[文字列] [必須] [選択してください: "flickr"]
|
|
34
|
+
-p, --plugin 実行するプラグイン[文字列] [必須] [選択してください: "flickr"]
|
|
32
35
|
-o, --options プラグインオプション [文字列] [デフォルト: "{}"]
|
|
33
36
|
-k, --keywords 検索キーワード(|区切り) [文字列] [デフォルト:
|
|
34
37
|
"nature,tree,flower|building,house|water,sea,river,pond"]
|
|
38
|
+
-f, --filed 大きなデータをファイルとして送受信する
|
|
39
|
+
[真偽] [デフォルト: true]
|
|
40
|
+
-c, --chopped 大きなデータを細分化して送受信する
|
|
41
|
+
[非推奨] [真偽] [デフォルト: false]
|
|
42
|
+
|
|
43
|
+
Debug
|
|
44
|
+
--debug-verbose デバッグ情報出力 [真偽] [デフォルト: false]
|
|
35
45
|
|
|
36
46
|
Visualization (最低一つの指定が必須です)
|
|
37
47
|
--vis-bulky 全データをCircleMarkerとして地図上に表示
|
|
@@ -41,7 +51,7 @@ Visualization (最低一つの指定が必須です)
|
|
|
41
51
|
|
|
42
52
|
オプション:
|
|
43
53
|
--help ヘルプを表示 [真偽]
|
|
44
|
-
--version バージョンを表示 [真偽]
|
|
54
|
+
--version バージョンを表示 [真偽]
|
|
45
55
|
```
|
|
46
56
|
## クローリングの実行
|
|
47
57
|
|
|
@@ -76,7 +86,7 @@ APIキーは以下の3種類の方法で与える事ができます
|
|
|
76
86
|
- 環境変数で渡す
|
|
77
87
|
- ```API_KEY_plugin```という環境変数に格納する
|
|
78
88
|
- コマンドに毎回含めなくて良くなる。
|
|
79
|
-
- **flickr
|
|
89
|
+
- **flickr**の場合は```API_KEY_flickr```になります。
|
|
80
90
|
- ```plugin```はプラグイン名(flickr等)に置き換えてください。
|
|
81
91
|
- 一時的な環境変数を定義する事も可能です。(bash等)
|
|
82
92
|
- ```API_KEY_flickr="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" node crawler.js -p flickr -k "sea,ocean|mountain,mount" --vis-bulky```
|
|
Binary file
|
package/crawler.js
CHANGED
|
@@ -8,7 +8,7 @@ import os from 'node:os';
|
|
|
8
8
|
import { EventEmitter } from 'node:events';
|
|
9
9
|
import path, { resolve, dirname } from 'node:path';
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
|
-
import { existsSync, constants } from 'node:fs';
|
|
11
|
+
import { existsSync, writeFileSync, constants } from 'node:fs';
|
|
12
12
|
import { access, readdir, readFile } from 'node:fs/promises';
|
|
13
13
|
|
|
14
14
|
// -------------------------------
|
|
@@ -28,6 +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, saveGeoJsonObjectAsStream } from './lib/splatone.js';
|
|
31
32
|
|
|
32
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
34
|
const __dirname = dirname(__filename);
|
|
@@ -107,6 +108,7 @@ try {
|
|
|
107
108
|
// コマンド例
|
|
108
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
|
|
109
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
|
|
110
112
|
let yargv = await yargs(hideBin(process.argv))
|
|
111
113
|
.strict() // 未定義オプションはエラー
|
|
112
114
|
.usage('使い方: $0 [options]')
|
|
@@ -131,6 +133,29 @@ try {
|
|
|
131
133
|
type: 'string',
|
|
132
134
|
default: 'nature,tree,flower|building,house|water,sea,river,pond',
|
|
133
135
|
description: '検索キーワード(|区切り)'
|
|
136
|
+
}).option('filed', {
|
|
137
|
+
group: 'Basic Options',
|
|
138
|
+
alias: 'f',
|
|
139
|
+
type: 'boolean',
|
|
140
|
+
default: true,
|
|
141
|
+
description: '大きなデータをファイルとして送受信する'
|
|
142
|
+
}).option('chopped', {
|
|
143
|
+
group: 'Basic Options',
|
|
144
|
+
alias: 'c',
|
|
145
|
+
type: 'boolean',
|
|
146
|
+
default: false,
|
|
147
|
+
deprecate: true,
|
|
148
|
+
description: '大きなデータを細分化して送受信する'
|
|
149
|
+
// }).option('debug-save', {
|
|
150
|
+
// group: 'Debug',
|
|
151
|
+
// type: 'boolean',
|
|
152
|
+
// default: false,
|
|
153
|
+
// description: 'サーバ側にクロールデータを保存'
|
|
154
|
+
}).option('debug-verbose', {
|
|
155
|
+
group: 'Debug',
|
|
156
|
+
type: 'boolean',
|
|
157
|
+
default: false,
|
|
158
|
+
description: 'デバッグ情報出力'
|
|
134
159
|
})
|
|
135
160
|
.version()
|
|
136
161
|
.coerce({
|
|
@@ -151,10 +176,13 @@ try {
|
|
|
151
176
|
if (Object.keys(all_visualizers).filter(v => argv["vis-" + v]).length == 0) {
|
|
152
177
|
throw new Error('可視化ツールの指定がありません。最低一つは指定してください。');
|
|
153
178
|
}
|
|
179
|
+
if (argv.filed && argv.chopped) {
|
|
180
|
+
console.warn("--filedと--choppedが両方指定されています。--filedが優先されます。");
|
|
181
|
+
argv.chopped = false;
|
|
182
|
+
}
|
|
154
183
|
return true;
|
|
155
184
|
});
|
|
156
185
|
const argv = await yargv.parseAsync();
|
|
157
|
-
|
|
158
186
|
const visualizers = {};
|
|
159
187
|
for (const vis of Object.keys(all_visualizers).filter(v => argv[`vis-${v}`])) {
|
|
160
188
|
visualizers[vis] = new all_visualizers[vis]();
|
|
@@ -164,11 +192,15 @@ try {
|
|
|
164
192
|
try {
|
|
165
193
|
plugin_options.API_KEY = await loadAPIKey("flickr") ?? plugin_options.API_KEY;
|
|
166
194
|
} catch (e) {
|
|
167
|
-
|
|
195
|
+
if (!plugin_options.API_KEY) {
|
|
196
|
+
console.error("Error loading API key:", e.message);
|
|
197
|
+
}
|
|
168
198
|
//Nothing to do
|
|
169
199
|
}
|
|
170
200
|
await plugins.call(argv.plugin, 'init', plugin_options);
|
|
171
|
-
|
|
201
|
+
if (argv.debugVerbose) {
|
|
202
|
+
console.table([["Visualizer", Object.keys(visualizers)], ["Plugin", argv.plugin]]);
|
|
203
|
+
}
|
|
172
204
|
|
|
173
205
|
/* API Key読み込み */
|
|
174
206
|
async function loadAPIKey(plugin = 'flickr') {
|
|
@@ -182,11 +214,11 @@ try {
|
|
|
182
214
|
await access(file, constants.F_OK | constants.R_OK);
|
|
183
215
|
// 読み込み & トリム
|
|
184
216
|
const raw = await readFile(file, 'utf8');
|
|
185
|
-
console.log(`[API KEY (${plugin}})] Read from FILE`);
|
|
217
|
+
//console.log(`[API KEY (${plugin}})] Read from FILE`);
|
|
186
218
|
key = raw.trim();
|
|
187
219
|
} catch (err) {
|
|
188
220
|
if (Object.prototype.hasOwnProperty.call(process.env, "API_KEY_" + plugin)) {
|
|
189
|
-
console.log(`[API KEY (${plugin}})] Read from ENV`);
|
|
221
|
+
//console.log(`[API KEY (${plugin}})] Read from ENV`);
|
|
190
222
|
key = process.env["API_KEY_" + plugin] ?? null;
|
|
191
223
|
} else {
|
|
192
224
|
const code = /** @type {{ code?: string }} */(err).code || 'UNKNOWN';
|
|
@@ -325,7 +357,10 @@ try {
|
|
|
325
357
|
console.warn("invalid sessionId:", req.sessionId);
|
|
326
358
|
return;
|
|
327
359
|
}
|
|
328
|
-
const { bbox, drawn, cellSize =
|
|
360
|
+
const { bbox, drawn, cellSize = 0, units = 'kilometers', tags = 'sea,beach|mountain,forest' } = req.query;
|
|
361
|
+
if (cellSize == 0) {
|
|
362
|
+
//セルサイズ自動決定()
|
|
363
|
+
}
|
|
329
364
|
const fallbackBbox = [139.55, 35.53, 139.92, 35.80];
|
|
330
365
|
let bboxArray = fallbackBbox;
|
|
331
366
|
|
|
@@ -527,7 +562,7 @@ try {
|
|
|
527
562
|
return piscina.run(data, { filename });
|
|
528
563
|
}
|
|
529
564
|
|
|
530
|
-
const nParallel = Math.max(1, Math.min(
|
|
565
|
+
const nParallel = Math.max(1, Math.min(6, os.cpus().length))
|
|
531
566
|
const piscina = new Piscina({
|
|
532
567
|
minThreads: 1,
|
|
533
568
|
maxThreads: nParallel,
|
|
@@ -547,7 +582,9 @@ try {
|
|
|
547
582
|
crawlers[p.sessionId][rtn.hexId][rtn.category].crawled ??= 0;
|
|
548
583
|
crawlers[p.sessionId][rtn.hexId][rtn.category].total = rtn.final ? crawlers[p.sessionId][rtn.hexId][rtn.category].ids.size : rtn.total + crawlers[p.sessionId][rtn.hexId][rtn.category].crawled;
|
|
549
584
|
crawlers[p.sessionId][rtn.hexId][rtn.category].crawled = crawlers[p.sessionId][rtn.hexId][rtn.category].ids.size;
|
|
550
|
-
|
|
585
|
+
if (argv.debugVerbose) {
|
|
586
|
+
console.log(`(CRAWL) ${rtn.hexId} ${rtn.category} ] dup=${duplicates.size}, out=${rtn.outside}, in=${rtn.photos.features.length} || ${crawlers[p.sessionId][rtn.hexId][rtn.category].crawled} / ${crawlers[p.sessionId][rtn.hexId][rtn.category].total}`);
|
|
587
|
+
}
|
|
551
588
|
const photos = featureCollection(rtn.photos.features.filter((f) => !duplicates.has(f.properties.id)));
|
|
552
589
|
crawlers[p.sessionId][rtn.hexId][rtn.category].items
|
|
553
590
|
= concatFC(crawlers[p.sessionId][rtn.hexId][rtn.category].items, photos);
|
|
@@ -559,22 +596,111 @@ try {
|
|
|
559
596
|
//console.log("next max_upload_date:", p.max_upload_date);
|
|
560
597
|
api.emit('splatone:start', p);
|
|
561
598
|
} else if (finish) {
|
|
562
|
-
|
|
599
|
+
if (argv.debugVerbose) {
|
|
600
|
+
console.table(stats);
|
|
601
|
+
}
|
|
563
602
|
api.emit('splatone:finish', p);
|
|
564
603
|
}
|
|
565
604
|
});
|
|
566
605
|
|
|
567
606
|
await subscribe('splatone:finish', async p => {
|
|
607
|
+
const resultId = uniqid();
|
|
568
608
|
const result = crawlers[p.sessionId];
|
|
569
609
|
const target = targets[p.sessionId];
|
|
610
|
+
|
|
611
|
+
let geoJson = Object.fromEntries(Object.entries(visualizers).map(([vis, v]) => [vis, v.getFutureCollection(result, target)]));
|
|
612
|
+
|
|
570
613
|
console.log('[splatone:finish]');
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
614
|
+
try {
|
|
615
|
+
if (argv.chopped || argv.filed) {
|
|
616
|
+
throw new RangeError("Invalid string length");
|
|
617
|
+
}
|
|
618
|
+
await io.to(p.sessionId).timeout(120000).emitWithAck('result', {
|
|
619
|
+
resultId,
|
|
620
|
+
geoJson,
|
|
621
|
+
palette: target["splatonePalette"],
|
|
622
|
+
visualizers: Object.keys(visualizers),
|
|
623
|
+
plugin: argv.plugin
|
|
624
|
+
});
|
|
625
|
+
} catch (e) {
|
|
626
|
+
if (e instanceof RangeError && /Invalid string length/.test(String(e.message))) {
|
|
627
|
+
const msg = ((argv.chopped || argv.filed) ? "ユーザの指定により" : "結果サイズが巨大なので") + (argv.chopped ? "断片化送信" : "保存ファイル送信") + "モードでクライアントに送ります";
|
|
628
|
+
if (argv.debugVerbose) {
|
|
629
|
+
console.warn("[WARN] " + msg);
|
|
630
|
+
}
|
|
631
|
+
io.to(p.sessionId).timeout(5000).emit('toast', {
|
|
632
|
+
text: msg,
|
|
633
|
+
class: "warning"
|
|
634
|
+
});
|
|
635
|
+
if (argv.chopped) {
|
|
636
|
+
//サイズ集計
|
|
637
|
+
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 })();
|
|
638
|
+
let current_features = 0
|
|
639
|
+
await dfsObject(geoJson, async ({ path, value, kind, type }) => {
|
|
640
|
+
if (path.length !== 0) {
|
|
641
|
+
if (kind === "primitive" || kind === "null") {
|
|
642
|
+
//console.log(path.join("."), "=>", `(${kind}:${type})`, value);
|
|
643
|
+
const ackrtn = await io.to(p.sessionId).timeout(120000).emitWithAck('result-chunk', {
|
|
644
|
+
resultId,
|
|
645
|
+
path,
|
|
646
|
+
kind,
|
|
647
|
+
type,
|
|
648
|
+
value
|
|
649
|
+
});
|
|
650
|
+
//console.log("\tACK", ackrtn);
|
|
651
|
+
} else if (kind === "object") {
|
|
652
|
+
//console.log(path.join("."), "=>", `(${kind}:${type})`);
|
|
653
|
+
if (path.at(-2) == "features" && Number.isInteger(path.at(-1))) {
|
|
654
|
+
current_features++;
|
|
655
|
+
}
|
|
656
|
+
const ackrtn = await io.to(p.sessionId).timeout(120000).emitWithAck('result-chunk', {
|
|
657
|
+
resultId,
|
|
658
|
+
path,
|
|
659
|
+
kind,
|
|
660
|
+
type,
|
|
661
|
+
progress: { current: current_features, total: total_features }
|
|
662
|
+
});
|
|
663
|
+
//console.log("\tACK", ackrtn);
|
|
664
|
+
} else if (kind === "array") {
|
|
665
|
+
//console.log(path.join("."), "=>", `(${kind}:${type})`);
|
|
666
|
+
const ackrtn = await io.to(p.sessionId).timeout(120000).emitWithAck('result-chunk', {
|
|
667
|
+
resultId,
|
|
668
|
+
path,
|
|
669
|
+
kind,
|
|
670
|
+
type
|
|
671
|
+
});
|
|
672
|
+
//console.log("\tACK", ackrtn);
|
|
673
|
+
}
|
|
674
|
+
} else {
|
|
675
|
+
//console.log("SKIP---------------------");
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
//console.log("finish chunks");
|
|
679
|
+
} else {
|
|
680
|
+
//保存ファイル送信(--filed)
|
|
681
|
+
try {
|
|
682
|
+
const outPath = await saveGeoJsonObjectAsStream(geoJson, 'result.' + resultId + '.json');
|
|
683
|
+
console.log('saved:', outPath);
|
|
684
|
+
const ackrtn = await io.to(p.sessionId).timeout(120000).emitWithAck('result-file', {
|
|
685
|
+
resultId,
|
|
686
|
+
});
|
|
687
|
+
} catch (err) {
|
|
688
|
+
console.error('failed:', err);
|
|
689
|
+
process.exitCode = 1;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
await io.to(p.sessionId).timeout(120000).emitWithAck('result', {
|
|
693
|
+
resultId,
|
|
694
|
+
geoJson: null, /*geoJsonは送らない*/
|
|
695
|
+
palette: target["splatonePalette"],
|
|
696
|
+
visualizers: Object.keys(visualizers),
|
|
697
|
+
plugin: argv.plugin
|
|
698
|
+
});
|
|
699
|
+
console.log("[Done]");
|
|
700
|
+
} else {
|
|
701
|
+
throw e; // 他の例外はそのまま
|
|
702
|
+
}
|
|
703
|
+
}
|
|
578
704
|
});
|
|
579
705
|
|
|
580
706
|
server.listen(port, async () => {
|
package/lib/paletteGenerator.js
CHANGED
|
@@ -61,7 +61,7 @@ const paletteGenerator = (function(undefined){
|
|
|
61
61
|
distanceType = 'Default';
|
|
62
62
|
ultra_precision = ultra_precision || false
|
|
63
63
|
|
|
64
|
-
console.log('Generate palettes for '+colorsCount+' colors using color distance "'+distanceType+'"')
|
|
64
|
+
//console.log('Generate palettes for '+colorsCount+' colors using color distance "'+distanceType+'"')
|
|
65
65
|
|
|
66
66
|
if(forceMode){
|
|
67
67
|
// Force Vector Mode
|
package/lib/splatone.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// 各種関数のまとめ場
|
|
2
|
+
// @turf/turf v6/v7 どちらでもOK(rhumbDistanceはv7で統合済み)
|
|
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';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* bbox の幅・高さを指定単位で返す
|
|
13
|
+
* @param {number[]} bbox [minX,minY,maxX,maxY] (経度・緯度, WGS84)
|
|
14
|
+
* @param {"meters"|"kilometers"|"miles"|"nauticalmiles"|"inches"|"yards"|"feet"} units
|
|
15
|
+
* @param {"geodesic"|"rhumb"} method 距離の定義:大円 or 等角航程
|
|
16
|
+
* @returns {{width:number, height:number, units:string}}
|
|
17
|
+
*/
|
|
18
|
+
export function bboxSize(bbox, units = "kilometers", method = "geodesic") {
|
|
19
|
+
const [minX, minY, maxX, maxY] = bbox;
|
|
20
|
+
|
|
21
|
+
// 中央経度・緯度(幅は中緯度、 高さは中経度で測るのが安定)
|
|
22
|
+
const midLat = (minY + maxY) / 2;
|
|
23
|
+
const midLon = (minX + maxX) / 2;
|
|
24
|
+
|
|
25
|
+
// 測りたい2点を作成
|
|
26
|
+
const west = point([minX, midLat]);
|
|
27
|
+
const east = point([maxX, midLat]);
|
|
28
|
+
const south = point([midLon, minY]);
|
|
29
|
+
const north = point([midLon, maxY]);
|
|
30
|
+
|
|
31
|
+
// 距離関数を選択
|
|
32
|
+
const distFn = method === "rhumb" ? rhumbDistance : distance;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
width: distFn(west, east, { units }),
|
|
36
|
+
height: distFn(south, north, { units }),
|
|
37
|
+
units
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* GeoJSON(Feature/FeatureCollection/Geometry)から計算したいときのヘルパー
|
|
43
|
+
*/
|
|
44
|
+
export function bboxSizeOf(geojson, units = "kilometers", method = "geodesic") {
|
|
45
|
+
return bboxSize(turfBbox(geojson), units, method);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
// visit({ key, value, path, parent, depth, kind, type, isLeaf, isNull })
|
|
50
|
+
// kind: 'array' | 'object' | 'primitive' | 'null'
|
|
51
|
+
// type: primitive は typeof、配列は 'Array'、オブジェクトは constructor.name
|
|
52
|
+
export async function dfsObject(root, visit, options = {}) {
|
|
53
|
+
const {
|
|
54
|
+
includeArrays = true,
|
|
55
|
+
maxDepth = Infinity,
|
|
56
|
+
stopOnTrue = false,
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
const meta = describeType(root);
|
|
60
|
+
|
|
61
|
+
// root が非オブジェクトの場合も visit だけ呼んで終了
|
|
62
|
+
if (meta.isLeaf) {
|
|
63
|
+
await visit({
|
|
64
|
+
key: undefined,
|
|
65
|
+
value: root,
|
|
66
|
+
path: [],
|
|
67
|
+
parent: null,
|
|
68
|
+
depth: 0,
|
|
69
|
+
...meta,
|
|
70
|
+
});
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const seen = new WeakSet();
|
|
75
|
+
const stack = [
|
|
76
|
+
{ key: undefined, value: root, path: /** @type {(string|number)[]} */([]), parent: null, depth: 0 }
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
while (stack.length) {
|
|
80
|
+
const node = stack.pop();
|
|
81
|
+
const { key, value, path, parent } = node;
|
|
82
|
+
|
|
83
|
+
const m = describeType(value);
|
|
84
|
+
const depth = path.length; // path 配列の長さをそのまま深さに
|
|
85
|
+
const ret = await visit({ key, value, path, parent, depth, ...m });
|
|
86
|
+
if (stopOnTrue && ret === true) return true;
|
|
87
|
+
|
|
88
|
+
if (m.isLeaf) continue; // primitive/null は展開しない
|
|
89
|
+
if (seen.has(value)) continue; // 循環参照対策
|
|
90
|
+
seen.add(value);
|
|
91
|
+
|
|
92
|
+
if (depth >= maxDepth) continue;
|
|
93
|
+
|
|
94
|
+
const isArr = Array.isArray(value);
|
|
95
|
+
if (!includeArrays && isArr) continue; // 配列を辿らない設定
|
|
96
|
+
|
|
97
|
+
const entries = isArr
|
|
98
|
+
? value.map((v, i) => [i, v]) // index は number
|
|
99
|
+
: Object.entries(value); // key は string
|
|
100
|
+
|
|
101
|
+
// 左→右の自然な DFS にしたいので逆順 push
|
|
102
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
103
|
+
const [k, v] = entries[i]; // k: string|number
|
|
104
|
+
const childPath = path.concat([k]);
|
|
105
|
+
stack.push({
|
|
106
|
+
key: k,
|
|
107
|
+
value: v,
|
|
108
|
+
path: childPath,
|
|
109
|
+
parent: value,
|
|
110
|
+
depth: childPath.length,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function describeType(value) {
|
|
118
|
+
if (value === null) {
|
|
119
|
+
return { kind: "null", type: "null", isLeaf: true, isNull: true };
|
|
120
|
+
}
|
|
121
|
+
const t = typeof value;
|
|
122
|
+
if (t !== "object") {
|
|
123
|
+
return {
|
|
124
|
+
kind: "primitive",
|
|
125
|
+
type: t, // 'number' | 'string' | 'boolean' | 'bigint' | 'symbol' | 'undefined'
|
|
126
|
+
isLeaf: true,
|
|
127
|
+
isNull: false,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
return { kind: "array", type: "Array", isLeaf: false, isNull: false };
|
|
132
|
+
}
|
|
133
|
+
const ctor = value?.constructor?.name ?? "Object";
|
|
134
|
+
return { kind: "object", type: ctor, isLeaf: false, isNull: false };
|
|
135
|
+
}
|
|
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
|
+
}
|
|
148
|
+
export default {
|
|
149
|
+
dfsObject, bboxSize, saveGeoJsonObjectAsStream
|
|
150
|
+
};
|
|
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.6",
|
|
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/plugins/flickr/worker.js
CHANGED
|
@@ -58,10 +58,11 @@ export default async function ({
|
|
|
58
58
|
= res.photos.photo.length > 0
|
|
59
59
|
? (res.photos.photo[res.photos.photo.length - 1].dateupload) - (res.photos.photo[res.photos.photo.length - 1].dateupload == res.photos.photo[0].dateupload ? 1 : 0)
|
|
60
60
|
: null;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const window = res.photos.photo.length == 0 ? 0 : res.photos.photo[0].dateupload - res.photos.photo[res.photos.photo.length - 1].dateupload;
|
|
62
|
+
if (Object.keys(authors).length == 1 && window < 60 * 60) {
|
|
63
|
+
const skip = window < 5 ? 0.1 : 12;
|
|
64
|
+
console.warn("[Warning]", `High posting activity detected for ${Object.keys(authors)} within ${window} s. the crawler will skip the next ${skip} hours.`);
|
|
65
|
+
next_max_upload_date -= 60 * 60 * skip;
|
|
65
66
|
}
|
|
66
67
|
return {
|
|
67
68
|
photos,
|
package/public/style.css
CHANGED
|
@@ -315,4 +315,14 @@ text-shadow: 0 1px 1px #0006;
|
|
|
315
315
|
.center-x {
|
|
316
316
|
display: flex;
|
|
317
317
|
justify-content: center; /* 横中央 */
|
|
318
|
-
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.toast-info {
|
|
321
|
+
background: linear-gradient(to right, #43cea2, #185a9d)!important;
|
|
322
|
+
}
|
|
323
|
+
.toast-warning{
|
|
324
|
+
background: linear-gradient(to bottom, #c21500, #ffc500)!important;
|
|
325
|
+
}
|
|
326
|
+
.toast-error {
|
|
327
|
+
background: linear-gradient(to bottom, #333333, #dd1818)!important;
|
|
328
|
+
}
|
package/public/visualizer.js
CHANGED
|
@@ -12,6 +12,21 @@ function addGeoJSONLayer(map, geojson, options = {}) {
|
|
|
12
12
|
return geojsonLayer;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function setAt(obj, path, value) {
|
|
16
|
+
if (path.length === 0) return value;
|
|
17
|
+
let cur = obj;
|
|
18
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
19
|
+
const k = path[i];
|
|
20
|
+
const nextK = path[i + 1];
|
|
21
|
+
if (cur[k] == null) {
|
|
22
|
+
// 次のキーが数値なら []、文字なら {}
|
|
23
|
+
cur[k] = Number.isInteger(nextK) ? [] : {};
|
|
24
|
+
}
|
|
25
|
+
cur = cur[k];
|
|
26
|
+
}
|
|
27
|
+
cur[path[path.length - 1]] = value;
|
|
28
|
+
return obj;
|
|
29
|
+
}
|
|
15
30
|
/*
|
|
16
31
|
使用例:
|
|
17
32
|
const layer = addGeoJSONLayer(map, myGeoJSON, {
|
|
@@ -46,4 +61,77 @@ function loadAsset(url) {
|
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
return Promise.reject(new Error(`Unsupported extension: ${url}`));
|
|
49
|
-
}
|
|
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
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="anonymous" />
|
|
12
12
|
<!-- Leaflet.draw CSS -->
|
|
13
13
|
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" crossorigin="anonymous" />
|
|
14
|
-
<link rel="stylesheet" href="
|
|
14
|
+
<link rel="stylesheet" href="style.css" />
|
|
15
|
+
<!-- Toastify -->
|
|
16
|
+
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
|
|
15
17
|
</head>
|
|
16
18
|
|
|
17
19
|
<body>
|
|
@@ -99,7 +101,8 @@
|
|
|
99
101
|
<!-- Map to Image -->
|
|
100
102
|
<script src="https://cdn.jsdelivr.net/npm/leaflet-easyprint@2.1.9/dist/bundle.min.js"></script>
|
|
101
103
|
<link href="https://cdn.jsdelivr.net/npm/leaflet-easyprint@2.1.9/libs/leaflet.min.css" rel="stylesheet">
|
|
102
|
-
|
|
104
|
+
<!-- Toastify -->
|
|
105
|
+
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
|
|
103
106
|
<!-- Socket.IO JS -->
|
|
104
107
|
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
|
105
108
|
<!-- Visualize.js -->
|
|
@@ -145,15 +148,21 @@
|
|
|
145
148
|
//console.log('Disconnected from server');
|
|
146
149
|
});
|
|
147
150
|
|
|
148
|
-
let sessionId=null;
|
|
151
|
+
let sessionId = null;
|
|
149
152
|
let visualizers = {};
|
|
150
|
-
let
|
|
151
|
-
|
|
153
|
+
let results = {};
|
|
154
|
+
let files = {};
|
|
155
|
+
let layerControl = null;
|
|
156
|
+
socket.on("welcome", async (res) => {
|
|
152
157
|
//console.log(`welcome ${res.sessionId} at ${new Date(res.time).toLocaleTimeString()}`);
|
|
153
|
-
console.log("VIS",res.visualizers);
|
|
158
|
+
//console.log("VIS", res.visualizers);
|
|
154
159
|
sessionId = res.sessionId;
|
|
155
|
-
visualizers = Object.fromEntries(await Promise.all(res.visualizers.map(async vis => {
|
|
156
|
-
|
|
160
|
+
visualizers = Object.fromEntries(await Promise.all(res.visualizers.map(async vis => {
|
|
161
|
+
const m = await import(new URL(`./visualizer/${vis}/web.js`,
|
|
162
|
+
import.meta.url).href);
|
|
163
|
+
return [vis, m.entry ?? m.default ?? m[vis]];
|
|
164
|
+
})));
|
|
165
|
+
//console.log(visualizers);
|
|
157
166
|
});
|
|
158
167
|
|
|
159
168
|
function generateLegend(legends) {
|
|
@@ -222,46 +231,124 @@
|
|
|
222
231
|
crawled: acc.crawled + (g.crawled ?? 0),
|
|
223
232
|
total: acc.total + (g.total ?? 0)
|
|
224
233
|
}), {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
234
|
+
crawled: 0,
|
|
235
|
+
total: 0
|
|
236
|
+
}
|
|
228
237
|
);
|
|
229
238
|
if (total == 0) {
|
|
230
239
|
crawled = 1, total = 1;
|
|
231
240
|
}
|
|
232
|
-
const percent = Math.round((crawled / total) *
|
|
233
|
-
console.log(`[Done] ${crawled} / ${total} --> ${percent}`);
|
|
241
|
+
const percent = Math.round((crawled / total) * 100000) / 1000;
|
|
242
|
+
//console.log(`[Done] ${crawled} / ${total} --> ${percent}`);
|
|
234
243
|
setProgress(document.getElementById("progressCrawl"), percent);
|
|
235
244
|
//ここでHexのプログレスグラデーションレイヤのOpacity調整
|
|
236
245
|
highlightHexById(res.hexId, 1 - res.progress[res.hexId].percent, "#263238");
|
|
237
246
|
});
|
|
238
247
|
|
|
239
248
|
function isPlainObject(a) {
|
|
240
|
-
if (a === null || typeof a !== 'object') return false;
|
|
249
|
+
if (a === null || typeof a !== 'object') return false; // 原始値/関数など除外
|
|
241
250
|
const proto = Object.getPrototypeOf(a);
|
|
242
|
-
return proto === Object.prototype || proto === null;
|
|
251
|
+
return proto === Object.prototype || proto === null; // {} / Object.create(null)
|
|
243
252
|
}
|
|
253
|
+
socket.on("toast", (arg) => {
|
|
254
|
+
const option = {
|
|
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
|
+
};
|
|
263
|
+
Toastify(option).showToast();
|
|
264
|
+
//https://apvarun.github.io/toastify-js/#
|
|
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
|
+
});
|
|
283
|
+
socket.on("result-chunk", (arg, callback) => {
|
|
284
|
+
if (arg.progress) {
|
|
285
|
+
setProgress(document.getElementById("progressCrawl"), Math.round((arg.progress.current / arg.progress.total) * 100000) / 1000);
|
|
286
|
+
}
|
|
287
|
+
results[arg.resultId] ??= {};
|
|
288
|
+
if (arg.kind === "primitive" || arg.kind === "null") {
|
|
289
|
+
//'number' | 'string' | 'boolean' | 'bigint' | 'symbol' | 'undefined'
|
|
290
|
+
//console.log(arg.path,arg.value);
|
|
291
|
+
switch (arg.type) {
|
|
292
|
+
case 'number':
|
|
293
|
+
setAt(results[arg.resultId], arg.path, Number(arg.value));
|
|
294
|
+
break;
|
|
295
|
+
case 'boolean':
|
|
296
|
+
setAt(results[arg.resultId], arg.path, arg.value);
|
|
297
|
+
case 'string':
|
|
298
|
+
setAt(results[arg.resultId], arg.path, String(arg.value));
|
|
299
|
+
break;
|
|
300
|
+
case 'bigint':
|
|
301
|
+
setAt(results[arg.resultId], arg.path, BigInt(arg.value));
|
|
302
|
+
break;
|
|
303
|
+
case 'symbol':
|
|
304
|
+
setAt(results[arg.resultId], arg.path, Symbol(arg.value));
|
|
305
|
+
break;
|
|
306
|
+
default:
|
|
307
|
+
setAt(results[arg.resultId], arg.path, null);
|
|
308
|
+
}
|
|
309
|
+
} else if (arg.kind === "object") {
|
|
310
|
+
setAt(results[arg.resultId], arg.path, {});
|
|
311
|
+
} else if (arg.kind === "array") {
|
|
312
|
+
setAt(results[arg.resultId], arg.path, []);
|
|
313
|
+
}
|
|
314
|
+
//console.log("ARK");
|
|
315
|
+
callback({
|
|
316
|
+
pong: Date.now(),
|
|
317
|
+
ok: true
|
|
318
|
+
}); // only one argument is expected
|
|
319
|
+
});
|
|
244
320
|
|
|
245
321
|
let latestResult = {};
|
|
322
|
+
let latestFile = null;
|
|
246
323
|
const overlays = {};
|
|
247
|
-
socket.on('result',async (res) => {
|
|
324
|
+
socket.on('result', async (res, callback) => {
|
|
325
|
+
//console.log("result");
|
|
248
326
|
latestResult["_"]["visualizers"] = res.visualizers;
|
|
249
327
|
latestResult["_"]["plugin"] = res.plugin;
|
|
250
328
|
clearHexHighlight();
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
329
|
+
if (res.geoJson == null) {
|
|
330
|
+
res.geoJson = results[res.resultId];
|
|
331
|
+
} else {
|
|
332
|
+
results[res.resultId] = res.geoJson;
|
|
333
|
+
}
|
|
334
|
+
latestResult = {
|
|
335
|
+
...latestResult,
|
|
336
|
+
...res.geoJson
|
|
337
|
+
};
|
|
338
|
+
for (const vis in visualizers) {
|
|
339
|
+
const layers = await visualizers[vis](map, res.geoJson[vis], {
|
|
340
|
+
palette: res.palette
|
|
341
|
+
});
|
|
342
|
+
//console.log("【レイヤ】\n", JSON.stringify(layers,null,4));
|
|
343
|
+
if ((layers == null)) {
|
|
258
344
|
//SKIP
|
|
259
|
-
}else if(layers.hasOwnProperty("type") && layers["type"]=="FeatureCollection"){
|
|
345
|
+
} else if (layers.hasOwnProperty("type") && layers["type"] == "FeatureCollection") {
|
|
260
346
|
//レイヤ一つ
|
|
261
|
-
overlays[`[${vis}]`] =
|
|
262
|
-
}else{
|
|
263
|
-
for(const name in layers){
|
|
264
|
-
|
|
347
|
+
overlays[`[${vis}]`] = layers;
|
|
348
|
+
} else {
|
|
349
|
+
for (const name in layers) {
|
|
350
|
+
console.log("Lay", vis, name);
|
|
351
|
+
overlays[`[${vis}] ${name}`] = layers[name];
|
|
265
352
|
}
|
|
266
353
|
}
|
|
267
354
|
}
|
|
@@ -280,7 +367,7 @@
|
|
|
280
367
|
});
|
|
281
368
|
}
|
|
282
369
|
ul.innerHTML = generateLegend(palette);
|
|
283
|
-
console.log(overlays);
|
|
370
|
+
//console.log(overlays);
|
|
284
371
|
layerControl = L.control.layers([baseLayer], {
|
|
285
372
|
...overlays,
|
|
286
373
|
"Hex Grid": hexLayer,
|
|
@@ -290,16 +377,24 @@
|
|
|
290
377
|
});
|
|
291
378
|
layerControl.addTo(map);
|
|
292
379
|
document.getElementById("map_legend")?.appendChild(ul);
|
|
293
|
-
document.getElementById("download-json").addEventListener("click", () => {
|
|
380
|
+
document.getElementById("download-json").addEventListener("click", async () => {
|
|
294
381
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
295
|
-
|
|
382
|
+
if(latestFile===null){
|
|
383
|
+
downloadJSON(`splatone-${stamp}.json`, latestResult);
|
|
384
|
+
}else{
|
|
385
|
+
await downloadJSONFile(`splatone-${stamp}.json`, latestFile);
|
|
386
|
+
}
|
|
296
387
|
let sessionId = null;
|
|
297
388
|
});
|
|
389
|
+
callback({
|
|
390
|
+
pong: Date.now(),
|
|
391
|
+
ok: true
|
|
392
|
+
});
|
|
298
393
|
});
|
|
299
394
|
|
|
300
395
|
// 地図
|
|
301
396
|
const map = L.map('map', {
|
|
302
|
-
preferCanvas:true,
|
|
397
|
+
preferCanvas: true,
|
|
303
398
|
zoomControl: true
|
|
304
399
|
}).setView([lat, lon], 12);
|
|
305
400
|
const baseLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
|
@@ -348,11 +443,11 @@
|
|
|
348
443
|
L.easyPrint({
|
|
349
444
|
title: 'マップの画像化とダウンロード',
|
|
350
445
|
position: 'bottomright',
|
|
351
|
-
sizeModes: [
|
|
446
|
+
sizeModes: ['A4Landscape'],
|
|
352
447
|
exportOnly: true,
|
|
353
448
|
filename: 'splatone_export'
|
|
354
449
|
}).addTo(map);
|
|
355
|
-
|
|
450
|
+
|
|
356
451
|
// レイヤ参照
|
|
357
452
|
let hexLayer = null;
|
|
358
453
|
let triLayer = null;
|
|
@@ -603,7 +698,7 @@
|
|
|
603
698
|
const cellSize = document.getElementById('cellSize').value.trim();
|
|
604
699
|
const units = document.getElementById('units').value.trim();
|
|
605
700
|
const keywords = document.getElementById('keywords').value.trim();
|
|
606
|
-
latestResult["_"]??={};
|
|
701
|
+
latestResult["_"] ??= {};
|
|
607
702
|
latestResult["_"]["keywords"] = keywords;
|
|
608
703
|
socket.once("hexgrid", (res) => {
|
|
609
704
|
if (res.sessionId) sessionId = res.sessionId;
|
|
@@ -611,7 +706,7 @@
|
|
|
611
706
|
const hex = res.hex;
|
|
612
707
|
const triangles = res.triangles;
|
|
613
708
|
latestResult["_"]["hex"] = hex;
|
|
614
|
-
latestResult["_"]["triangles"]=[triangles];
|
|
709
|
+
latestResult["_"]["triangles"] = [triangles];
|
|
615
710
|
renderLayers(hex, triangles);
|
|
616
711
|
});
|
|
617
712
|
socket.emit("target", {
|
package/visualizer/bulky/node.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { VisualizerBase } from '../../lib/VisualizerBase.js';
|
|
2
2
|
import { featureCollection } from "@turf/turf";
|
|
3
|
-
import { writeFileSync } from 'node:fs';
|
|
4
3
|
|
|
5
4
|
export default class BulkyVisualizer extends VisualizerBase {
|
|
6
5
|
static id = 'bulky'; // 一意ID(フォルダ名と一致させると運用しやすい)
|
package/visualizer/bulky/web.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
let booted = false;
|
|
2
2
|
export default async function main(map, geojson, options = {}) {
|
|
3
|
+
console.log("main");
|
|
3
4
|
if (booted) return;
|
|
4
5
|
booted = true;
|
|
5
6
|
const layers = {};
|
|
6
7
|
for (const cat in geojson) {
|
|
8
|
+
console.log(cat);
|
|
7
9
|
const layer = addGeoJSONLayer(map, geojson[cat], {
|
|
8
10
|
pointToLayer: (feature, latlng) => {
|
|
9
11
|
return L.circleMarker(latlng, {
|