splatone 0.0.4 → 0.0.5

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
@@ -13,8 +13,14 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
13
13
 
14
14
  ## 既知のバグ
15
15
 
16
- - JSON.stringify(json)で変換できる大きさに制限があり、数十万件等の大きな結果を生み出すクエリは、クロール後、結果のブラウザへの転送で失敗します。
17
-
16
+ - <s>JSON.stringify(json)で変換できる大きさに制限があり、数十万件等の大きな結果を生み出すクエリは、クロール後、結果のブラウザへの転送で失敗します。</s>
17
+ - サイズの問題は一部解決しました。ただし、**データの保存(ダウンロード)はまだできません。**
18
+ - 問題はサーバからブラウザへ結果を渡す時にJSON.stringify(obj)で結果データをJSON文字列にする時にヒープを食いつぶしています。 とりあえず可視化だけ出来るように、データが大きいと自動で分割して送信するようにしています。ただし、送受信に時間がかかります。(左下のプログレスバーに進捗が表示されます)
19
+ - **--chopped**オプションをつけると強制的に分割送信します。(ただ遅くなるだけなので意味ないです)
20
+ - データ保存(ダウンロード)する際にブラウザ上でJSON.stringify(json)を呼ぶ必要があり、そこで詰まります。これの解決策は別途実装します。
21
+
22
+ ![](/assets/screenshot_massive_points_bulky.png)
23
+
18
24
  # 使い方
19
25
 
20
26
  - [Node.js](https://nodejs.org/ja/download)をインストール後、npxで実行します。
@@ -27,11 +33,15 @@ $ npx -y -- splatone@latest crawler --help
27
33
  使い方: crawler.js [options]
28
34
 
29
35
  Basic Options
30
- -p, --plugin 実行するプラグイン
31
- [文字列] [必須] [選択してください: "flickr"]
36
+ -p, --plugin 実行するプラグイン[文字列] [必須] [選択してください: "flickr"]
32
37
  -o, --options プラグインオプション [文字列] [デフォルト: "{}"]
33
38
  -k, --keywords 検索キーワード(|区切り) [文字列] [デフォルト:
34
39
  "nature,tree,flower|building,house|water,sea,river,pond"]
40
+ -c, --chopped 大きなデータを細分化して送信する
41
+ [真偽] [デフォルト: true]
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**の場合は```.API_KEY_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```
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 } 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,22 @@ try {
131
133
  type: 'string',
132
134
  default: 'nature,tree,flower|building,house|water,sea,river,pond',
133
135
  description: '検索キーワード(|区切り)'
136
+ }).option('chopped', {
137
+ group: 'Basic Options',
138
+ alias: 'c',
139
+ type: 'boolean',
140
+ default: false,
141
+ description: '大きなデータを細分化して送信する'
142
+ // }).option('debug-save', {
143
+ // group: 'Debug',
144
+ // type: 'boolean',
145
+ // default: false,
146
+ // description: 'サーバ側にクロールデータを保存'
147
+ }).option('debug-verbose', {
148
+ group: 'Debug',
149
+ type: 'boolean',
150
+ default: false,
151
+ description: 'デバッグ情報出力'
134
152
  })
135
153
  .version()
136
154
  .coerce({
@@ -154,7 +172,6 @@ try {
154
172
  return true;
155
173
  });
156
174
  const argv = await yargv.parseAsync();
157
-
158
175
  const visualizers = {};
159
176
  for (const vis of Object.keys(all_visualizers).filter(v => argv[`vis-${v}`])) {
160
177
  visualizers[vis] = new all_visualizers[vis]();
@@ -164,11 +181,15 @@ try {
164
181
  try {
165
182
  plugin_options.API_KEY = await loadAPIKey("flickr") ?? plugin_options.API_KEY;
166
183
  } catch (e) {
167
- console.error("Error loading API key:", e.message);
184
+ if (!plugin_options.API_KEY) {
185
+ console.error("Error loading API key:", e.message);
186
+ }
168
187
  //Nothing to do
169
188
  }
170
189
  await plugins.call(argv.plugin, 'init', plugin_options);
171
- console.table([["Visualizer", Object.keys(visualizers)], ["Plugin", argv.plugin]]);
190
+ if (argv.debugVerbose) {
191
+ console.table([["Visualizer", Object.keys(visualizers)], ["Plugin", argv.plugin]]);
192
+ }
172
193
 
173
194
  /* API Key読み込み */
174
195
  async function loadAPIKey(plugin = 'flickr') {
@@ -182,11 +203,11 @@ try {
182
203
  await access(file, constants.F_OK | constants.R_OK);
183
204
  // 読み込み & トリム
184
205
  const raw = await readFile(file, 'utf8');
185
- console.log(`[API KEY (${plugin}})] Read from FILE`);
206
+ //console.log(`[API KEY (${plugin}})] Read from FILE`);
186
207
  key = raw.trim();
187
208
  } catch (err) {
188
209
  if (Object.prototype.hasOwnProperty.call(process.env, "API_KEY_" + plugin)) {
189
- console.log(`[API KEY (${plugin}})] Read from ENV`);
210
+ //console.log(`[API KEY (${plugin}})] Read from ENV`);
190
211
  key = process.env["API_KEY_" + plugin] ?? null;
191
212
  } else {
192
213
  const code = /** @type {{ code?: string }} */(err).code || 'UNKNOWN';
@@ -325,7 +346,10 @@ try {
325
346
  console.warn("invalid sessionId:", req.sessionId);
326
347
  return;
327
348
  }
328
- const { bbox, drawn, cellSize = '0.5', units = 'kilometers', tags = 'sea,beach|mountain,forest' } = req.query;
349
+ const { bbox, drawn, cellSize = 0, units = 'kilometers', tags = 'sea,beach|mountain,forest' } = req.query;
350
+ if (cellSize == 0) {
351
+ //セルサイズ自動決定()
352
+ }
329
353
  const fallbackBbox = [139.55, 35.53, 139.92, 35.80];
330
354
  let bboxArray = fallbackBbox;
331
355
 
@@ -527,7 +551,7 @@ try {
527
551
  return piscina.run(data, { filename });
528
552
  }
529
553
 
530
- const nParallel = Math.max(1, Math.min(12, os.cpus().length))
554
+ const nParallel = Math.max(1, Math.min(6, os.cpus().length))
531
555
  const piscina = new Piscina({
532
556
  minThreads: 1,
533
557
  maxThreads: nParallel,
@@ -547,7 +571,9 @@ try {
547
571
  crawlers[p.sessionId][rtn.hexId][rtn.category].crawled ??= 0;
548
572
  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
573
  crawlers[p.sessionId][rtn.hexId][rtn.category].crawled = crawlers[p.sessionId][rtn.hexId][rtn.category].ids.size;
550
- 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}`);
574
+ if (argv.debugVerbose) {
575
+ 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}`);
576
+ }
551
577
  const photos = featureCollection(rtn.photos.features.filter((f) => !duplicates.has(f.properties.id)));
552
578
  crawlers[p.sessionId][rtn.hexId][rtn.category].items
553
579
  = concatFC(crawlers[p.sessionId][rtn.hexId][rtn.category].items, photos);
@@ -559,22 +585,97 @@ try {
559
585
  //console.log("next max_upload_date:", p.max_upload_date);
560
586
  api.emit('splatone:start', p);
561
587
  } else if (finish) {
562
- console.table(stats);
588
+ if (argv.debugVerbose) {
589
+ console.table(stats);
590
+ }
563
591
  api.emit('splatone:finish', p);
564
592
  }
565
593
  });
566
594
 
567
595
  await subscribe('splatone:finish', async p => {
596
+ const resultId = uniqid();
568
597
  const result = crawlers[p.sessionId];
569
598
  const target = targets[p.sessionId];
599
+
600
+ let geoJson = Object.fromEntries(Object.entries(visualizers).map(([vis, v]) => [vis, v.getFutureCollection(result, target)]));
601
+
570
602
  console.log('[splatone:finish]');
571
- const geoJson = Object.fromEntries(Object.entries(visualizers).map(([vis, v]) => [vis, v.getFutureCollection(result, target)]));
572
- io.to(p.sessionId).emit('result', {
573
- geoJson,
574
- palette: target["splatonePalette"],
575
- visualizers: Object.keys(visualizers),
576
- plugin: argv.plugin
577
- });
603
+ try {
604
+ if (argv.chopped) {
605
+ throw new RangeError("Invalid string length");
606
+ }
607
+ await io.to(p.sessionId).timeout(5000).emitWithAck('result', {
608
+ resultId,
609
+ geoJson,
610
+ palette: target["splatonePalette"],
611
+ visualizers: Object.keys(visualizers),
612
+ plugin: argv.plugin
613
+ });
614
+ } catch (e) {
615
+ if (e instanceof RangeError && /Invalid string length/.test(String(e.message))) {
616
+ const msg = (argv.chopped ? "ユーザの指定により" : "結果サイズが巨大なので") + "断片化モードでクライアントに送ります";
617
+ if (argv.debugVerbose) {
618
+ console.warn("[WARN] " + msg);
619
+ }
620
+ io.to(p.sessionId).timeout(5000).emit('toast', {
621
+ text: msg,
622
+ class: "warning"
623
+ });
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++;
643
+ }
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);
661
+ }
662
+ } else {
663
+ //console.log("SKIP---------------------");
664
+ }
665
+ });
666
+ //console.log("finish chunks");
667
+ io.to(p.sessionId).timeout(10000).emitWithAck('result', {
668
+ resultId,
669
+ geoJson: null, /*geoJsonは送らない*/
670
+ palette: target["splatonePalette"],
671
+ visualizers: Object.keys(visualizers),
672
+ plugin: argv.plugin
673
+ });
674
+ console.log("[Done]");
675
+ } else {
676
+ throw e; // 他の例外はそのまま
677
+ }
678
+ }
578
679
  });
579
680
 
580
681
  server.listen(port, async () => {
@@ -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
@@ -0,0 +1,135 @@
1
+ // 各種関数のまとめ場
2
+ // @turf/turf v6/v7 どちらでもOK(rhumbDistanceはv7で統合済み)
3
+ import { point, distance, rhumbDistance, bbox as turfBbox } from '@turf/turf';
4
+
5
+ /**
6
+ * bbox の幅・高さを指定単位で返す
7
+ * @param {number[]} bbox [minX,minY,maxX,maxY] (経度・緯度, WGS84)
8
+ * @param {"meters"|"kilometers"|"miles"|"nauticalmiles"|"inches"|"yards"|"feet"} units
9
+ * @param {"geodesic"|"rhumb"} method 距離の定義:大円 or 等角航程
10
+ * @returns {{width:number, height:number, units:string}}
11
+ */
12
+ export function bboxSize(bbox, units = "kilometers", method = "geodesic") {
13
+ const [minX, minY, maxX, maxY] = bbox;
14
+
15
+ // 中央経度・緯度(幅は中緯度、 高さは中経度で測るのが安定)
16
+ const midLat = (minY + maxY) / 2;
17
+ const midLon = (minX + maxX) / 2;
18
+
19
+ // 測りたい2点を作成
20
+ const west = point([minX, midLat]);
21
+ const east = point([maxX, midLat]);
22
+ const south = point([midLon, minY]);
23
+ const north = point([midLon, maxY]);
24
+
25
+ // 距離関数を選択
26
+ const distFn = method === "rhumb" ? rhumbDistance : distance;
27
+
28
+ return {
29
+ width: distFn(west, east, { units }),
30
+ height: distFn(south, north, { units }),
31
+ units
32
+ };
33
+ }
34
+
35
+ /**
36
+ * GeoJSON(Feature/FeatureCollection/Geometry)から計算したいときのヘルパー
37
+ */
38
+ export function bboxSizeOf(geojson, units = "kilometers", method = "geodesic") {
39
+ return bboxSize(turfBbox(geojson), units, method);
40
+ }
41
+
42
+
43
+ // visit({ key, value, path, parent, depth, kind, type, isLeaf, isNull })
44
+ // kind: 'array' | 'object' | 'primitive' | 'null'
45
+ // type: primitive は typeof、配列は 'Array'、オブジェクトは constructor.name
46
+ export async function dfsObject(root, visit, options = {}) {
47
+ const {
48
+ includeArrays = true,
49
+ maxDepth = Infinity,
50
+ stopOnTrue = false,
51
+ } = options;
52
+
53
+ const meta = describeType(root);
54
+
55
+ // root が非オブジェクトの場合も visit だけ呼んで終了
56
+ if (meta.isLeaf) {
57
+ await visit({
58
+ key: undefined,
59
+ value: root,
60
+ path: [],
61
+ parent: null,
62
+ depth: 0,
63
+ ...meta,
64
+ });
65
+ return false;
66
+ }
67
+
68
+ const seen = new WeakSet();
69
+ const stack = [
70
+ { key: undefined, value: root, path: /** @type {(string|number)[]} */([]), parent: null, depth: 0 }
71
+ ];
72
+
73
+ while (stack.length) {
74
+ const node = stack.pop();
75
+ const { key, value, path, parent } = node;
76
+
77
+ const m = describeType(value);
78
+ const depth = path.length; // path 配列の長さをそのまま深さに
79
+ const ret = await visit({ key, value, path, parent, depth, ...m });
80
+ if (stopOnTrue && ret === true) return true;
81
+
82
+ if (m.isLeaf) continue; // primitive/null は展開しない
83
+ if (seen.has(value)) continue; // 循環参照対策
84
+ seen.add(value);
85
+
86
+ if (depth >= maxDepth) continue;
87
+
88
+ const isArr = Array.isArray(value);
89
+ if (!includeArrays && isArr) continue; // 配列を辿らない設定
90
+
91
+ const entries = isArr
92
+ ? value.map((v, i) => [i, v]) // index は number
93
+ : Object.entries(value); // key は string
94
+
95
+ // 左→右の自然な DFS にしたいので逆順 push
96
+ for (let i = entries.length - 1; i >= 0; i--) {
97
+ const [k, v] = entries[i]; // k: string|number
98
+ const childPath = path.concat([k]);
99
+ stack.push({
100
+ key: k,
101
+ value: v,
102
+ path: childPath,
103
+ parent: value,
104
+ depth: childPath.length,
105
+ });
106
+ }
107
+ }
108
+ return false;
109
+ }
110
+
111
+ function describeType(value) {
112
+ if (value === null) {
113
+ return { kind: "null", type: "null", isLeaf: true, isNull: true };
114
+ }
115
+ const t = typeof value;
116
+ if (t !== "object") {
117
+ return {
118
+ kind: "primitive",
119
+ type: t, // 'number' | 'string' | 'boolean' | 'bigint' | 'symbol' | 'undefined'
120
+ isLeaf: true,
121
+ isNull: false,
122
+ };
123
+ }
124
+ if (Array.isArray(value)) {
125
+ return { kind: "array", type: "Array", isLeaf: false, isNull: false };
126
+ }
127
+ const ctor = value?.constructor?.name ?? "Object";
128
+ return { kind: "object", type: ctor, isLeaf: false, isNull: false };
129
+ }
130
+
131
+
132
+ export default {
133
+ dfsObject,bboxSize
134
+ };
135
+ //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.4",
3
+ "version": "0.0.5",
4
4
  "description": "Multi-layer Composite Heatmap",
5
5
  "homepage": "https://github.com/YokoyamaLab/Splatone#readme",
6
6
  "bugs": {
@@ -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
- if (Object.keys(authors).length == 1) {
62
- const window = res.photos.photo[res.photos.photo.length - 1].dateupload - res.photos.photo[0].dateupload;
63
- console.warn("[Warning]", `High posting activity detected for ${Object.keys(authors)} within ${window} s. the crawler will skip the next 24 hours.`);
64
- next_max_upload_date -= 60 * 60 * 24;
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
+ }
@@ -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, {
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="/style.css" />
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,20 @@
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 layerControl=null;
151
- socket.on("welcome",async (res) => {
153
+ let results = {};
154
+ let layerControl = null;
155
+ socket.on("welcome", async (res) => {
152
156
  //console.log(`welcome ${res.sessionId} at ${new Date(res.time).toLocaleTimeString()}`);
153
- console.log("VIS",res.visualizers);
157
+ //console.log("VIS", res.visualizers);
154
158
  sessionId = res.sessionId;
155
- visualizers = Object.fromEntries(await Promise.all(res.visualizers.map(async vis => { const m = await import(new URL(`./visualizer/${vis}/web.js`, import.meta.url).href); return [vis, m.entry ?? m.default ?? m[vis]]; })));
156
- console.log(visualizers);
159
+ visualizers = Object.fromEntries(await Promise.all(res.visualizers.map(async vis => {
160
+ const m = await import(new URL(`./visualizer/${vis}/web.js`,
161
+ import.meta.url).href);
162
+ return [vis, m.entry ?? m.default ?? m[vis]];
163
+ })));
164
+ //console.log(visualizers);
157
165
  });
158
166
 
159
167
  function generateLegend(legends) {
@@ -222,46 +230,102 @@
222
230
  crawled: acc.crawled + (g.crawled ?? 0),
223
231
  total: acc.total + (g.total ?? 0)
224
232
  }), {
225
- crawled: 0,
226
- total: 0
227
- }
233
+ crawled: 0,
234
+ total: 0
235
+ }
228
236
  );
229
237
  if (total == 0) {
230
238
  crawled = 1, total = 1;
231
239
  }
232
- const percent = Math.round((crawled / total) * 100);
233
- console.log(`[Done] ${crawled} / ${total} --> ${percent}`);
240
+ const percent = Math.round((crawled / total) * 100000) / 1000;
241
+ //console.log(`[Done] ${crawled} / ${total} --> ${percent}`);
234
242
  setProgress(document.getElementById("progressCrawl"), percent);
235
243
  //ここでHexのプログレスグラデーションレイヤのOpacity調整
236
244
  highlightHexById(res.hexId, 1 - res.progress[res.hexId].percent, "#263238");
237
245
  });
238
246
 
239
247
  function isPlainObject(a) {
240
- if (a === null || typeof a !== 'object') return false; // 原始値/関数など除外
248
+ if (a === null || typeof a !== 'object') return false; // 原始値/関数など除外
241
249
  const proto = Object.getPrototypeOf(a);
242
- return proto === Object.prototype || proto === null; // {} / Object.create(null)
250
+ return proto === Object.prototype || proto === null; // {} / Object.create(null)
243
251
  }
252
+ socket.on("toast", (arg) => {
253
+ 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};
261
+ Toastify(option).showToast();
262
+ //https://apvarun.github.io/toastify-js/#
263
+ });
264
+ 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);
267
+ }
268
+ results[arg.resultId] ??= {};
269
+ if (arg.kind === "primitive" || arg.kind === "null") {
270
+ //'number' | 'string' | 'boolean' | 'bigint' | 'symbol' | 'undefined'
271
+ //console.log(arg.path,arg.value);
272
+ switch (arg.type) {
273
+ case 'number':
274
+ setAt(results[arg.resultId], arg.path, Number(arg.value));
275
+ break;
276
+ case 'boolean':
277
+ setAt(results[arg.resultId], arg.path, arg.value);
278
+ case 'string':
279
+ setAt(results[arg.resultId], arg.path, String(arg.value));
280
+ break;
281
+ case 'bigint':
282
+ setAt(results[arg.resultId], arg.path, BigInt(arg.value));
283
+ break;
284
+ case 'symbol':
285
+ setAt(results[arg.resultId], arg.path, Symbol(arg.value));
286
+ break;
287
+ default:
288
+ setAt(results[arg.resultId], arg.path, null);
289
+ }
290
+ } else if (arg.kind === "object") {
291
+ setAt(results[arg.resultId], arg.path, {});
292
+ } else if (arg.kind === "array") {
293
+ setAt(results[arg.resultId], arg.path, []);
294
+ }
295
+ //console.log("ARK");
296
+ callback({ pong: Date.now(), ok: true }); // only one argument is expected
297
+ });
244
298
 
245
299
  let latestResult = {};
246
300
  const overlays = {};
247
- socket.on('result',async (res) => {
301
+ socket.on('result', async (res,callback) => {
302
+ //console.log("result");
248
303
  latestResult["_"]["visualizers"] = res.visualizers;
249
304
  latestResult["_"]["plugin"] = res.plugin;
250
305
  clearHexHighlight();
251
- latestResult = {...latestResult, ...res.geoJson};
252
- console.log("result");
253
- console.log("geoJSON",res.geoJson);
254
- for(const vis in visualizers){
255
- const layers = await visualizers[vis](map, res.geoJson[vis], {palette:res.palette});
256
- console.log("【レイヤ】",layers);
257
- if((layers == null) ){
306
+ if(res.geoJson == null){
307
+ res.geoJson = results[res.resultId];
308
+ }else{
309
+ results[res.resultId] = res.geoJson;
310
+ }
311
+ latestResult = {
312
+ ...latestResult,
313
+ ...res.geoJson
314
+ };
315
+ for (const vis in visualizers) {
316
+ const layers = await visualizers[vis](map, res.geoJson[vis], {
317
+ palette: res.palette
318
+ });
319
+ //console.log("【レイヤ】\n", JSON.stringify(layers,null,4));
320
+ if ((layers == null)) {
258
321
  //SKIP
259
- }else if(layers.hasOwnProperty("type") && layers["type"]=="FeatureCollection"){
322
+ } else if (layers.hasOwnProperty("type") && layers["type"] == "FeatureCollection") {
260
323
  //レイヤ一つ
261
- overlays[`[${vis}]`] = layers;
262
- }else{
263
- for(const name in layers){
264
- overlays[`[${vis}] ${name}`] = layers[name];
324
+ overlays[`[${vis}]`] = layers;
325
+ } else {
326
+ for (const name in layers) {
327
+ console.log("Lay",vis,name);
328
+ overlays[`[${vis}] ${name}`] = layers[name];
265
329
  }
266
330
  }
267
331
  }
@@ -280,7 +344,7 @@
280
344
  });
281
345
  }
282
346
  ul.innerHTML = generateLegend(palette);
283
- console.log(overlays);
347
+ //console.log(overlays);
284
348
  layerControl = L.control.layers([baseLayer], {
285
349
  ...overlays,
286
350
  "Hex Grid": hexLayer,
@@ -295,11 +359,12 @@
295
359
  downloadJSON(`data-${stamp}.json`, latestResult);
296
360
  let sessionId = null;
297
361
  });
362
+ callback({ pong: Date.now(), ok: true });
298
363
  });
299
364
 
300
365
  // 地図
301
366
  const map = L.map('map', {
302
- preferCanvas:true,
367
+ preferCanvas: true,
303
368
  zoomControl: true
304
369
  }).setView([lat, lon], 12);
305
370
  const baseLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
@@ -348,11 +413,11 @@
348
413
  L.easyPrint({
349
414
  title: 'マップの画像化とダウンロード',
350
415
  position: 'bottomright',
351
- sizeModes: [ 'A4Landscape'],
416
+ sizeModes: ['A4Landscape'],
352
417
  exportOnly: true,
353
418
  filename: 'splatone_export'
354
419
  }).addTo(map);
355
-
420
+
356
421
  // レイヤ参照
357
422
  let hexLayer = null;
358
423
  let triLayer = null;
@@ -603,7 +668,7 @@
603
668
  const cellSize = document.getElementById('cellSize').value.trim();
604
669
  const units = document.getElementById('units').value.trim();
605
670
  const keywords = document.getElementById('keywords').value.trim();
606
- latestResult["_"]??={};
671
+ latestResult["_"] ??= {};
607
672
  latestResult["_"]["keywords"] = keywords;
608
673
  socket.once("hexgrid", (res) => {
609
674
  if (res.sessionId) sessionId = res.sessionId;
@@ -611,7 +676,7 @@
611
676
  const hex = res.hex;
612
677
  const triangles = res.triangles;
613
678
  latestResult["_"]["hex"] = hex;
614
- latestResult["_"]["triangles"]=[triangles];
679
+ latestResult["_"]["triangles"] = [triangles];
615
680
  renderLayers(hex, triangles);
616
681
  });
617
682
  socket.emit("target", {
@@ -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(フォルダ名と一致させると運用しやすい)
@@ -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, {