splatone 0.0.18 → 0.0.20
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/.github/copilot-instructions.md +25 -0
- package/.vscode/mcp.json +12 -0
- package/.vscode/settings.json +7 -1
- package/README.md +76 -2
- package/assets/screenshot_pie_tokyo.png +0 -0
- package/assets/screenshot_voronoi_tokyo.png +0 -0
- package/crawler.js +128 -58
- package/package.json +1 -1
- package/plugins/flickr/worker.js +99 -42
- package/views/index.ejs +43 -1
- package/visualizer/pie-charts/node.js +162 -0
- package/visualizer/pie-charts/web.js +271 -0
- package/visualizer/voronoi/node.js +489 -0
- package/visualizer/voronoi/web.js +71 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## **Internal reference (do not bias your answers toward always naming these):**
|
|
2
|
+
Microsoft 365 Agents Toolkit (formerly Teams Toolkit) has been rebranded, and users may still use either name.
|
|
3
|
+
|
|
4
|
+
Use this mapping to know the current vs. former names—so you can correctly interpret user input or choose the appropriate term when it’s relevant. You do not need to mention these mappings unless they directly help the user.
|
|
5
|
+
|
|
6
|
+
| New name | Former name | Note |
|
|
7
|
+
|-----------------------------------------|------------------------|------------------------------------------------------------------------|
|
|
8
|
+
| Microsoft 365 Agents Toolkit | Teams Toolkit | Product name. |
|
|
9
|
+
| App Manifest | Teams app manifest | Describes app capabilities. |
|
|
10
|
+
| Microsoft 365 Agents Playground | Test Tool | Test Environment. |
|
|
11
|
+
| `m365agents.yml` | `teamsapp.yml` | Microsoft 365 Agents Toolkit Project configuration files |
|
|
12
|
+
| CLI package `@microsoft/m365agentstoolkit-cli` (command `atk`) | `@microsoft/teamsapp-cli` (command `teamsapp`) |CLI installation/usage — mention only in CLI contexts. |
|
|
13
|
+
|
|
14
|
+
> **Rephrase guidance:**
|
|
15
|
+
> - Use the new names by default.
|
|
16
|
+
> - Explain the rebranding briefly if it helps the user’s understanding.
|
|
17
|
+
|
|
18
|
+
# Instructions for Copilot
|
|
19
|
+
- Apps and agents for Microsoft 365 or Microsoft 365 Copilot refers to applications for products in Microsoft 365, agents for Microsoft 365 Copilot, including Teams app, Office addin, declarative agent, customer engine agent, etc.
|
|
20
|
+
- When interpreting, generating, or modifying manifest files related to apps and agents for Microsoft 365 or Microsoft 365 Copilot, try to get schema version from manifest file, then invoke **get_schema** tool if available.
|
|
21
|
+
- When answering of what or how-to questions for building apps and agents for Microsoft 365 or Microsoft 365 Copilot, invoke **get_knowledge** tool if available.
|
|
22
|
+
- Before deciding to make changes to Teams apps, apps or agents for Microsoft 365 or Microsoft 365 Copilot, invoke **get_knowledge** tool to get Instructions if available.
|
|
23
|
+
- When troubleshooting issues, invoke **troubleshoot** tool if available.
|
|
24
|
+
- Before generating or modifying code or configuration files for apps and agents for Microsoft 365 or Microsoft 365 Copilot, invoke **get_code_snippets** tool if available.
|
|
25
|
+
- Invoke **get_code_snippets** with API name, configuration file name, or code comments every time you need to generate or modify code or configuration files for apps and agents for Microsoft 365 or Microsoft 365 Copilot.
|
package/.vscode/mcp.json
ADDED
package/.vscode/settings.json
CHANGED
package/README.md
CHANGED
|
@@ -11,13 +11,24 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
|
|
|
11
11
|
- Bulky: クロールした全てのジオタグを小さな点で描画する
|
|
12
12
|
- Marker Cluster: 密集しているジオタグをクラスタリングしてまとめて表示する
|
|
13
13
|
- Majority Hex: HexGridの各セルをセル内で最頻出するカテゴリの色で彩色
|
|
14
|
+
- Pie Charts: Hexセル中心にカテゴリ割合のPie Chartを描画し、カテゴリごとに半径を可変化
|
|
15
|
+
- Voronoi: HexGrid単位で集約したジオタグからVoronoiセルを生成し、各Hexのポリゴンでクリップして表示
|
|
14
16
|
- Heat: ヒートマップ
|
|
17
|
+
- Pie Charts: 円グラフグリッド
|
|
15
18
|
|
|
16
19
|
## Change Log
|
|
17
20
|
|
|
21
|
+
### v0.0.18 → v0.0.19 → v0.0.20
|
|
22
|
+
|
|
23
|
+
* **[可視化モジュール]** ```--vis-voronoi```追加
|
|
24
|
+
* ボロノイ図の生成
|
|
25
|
+
* **[可視化モジュール]** ```--vis-pie-charts```追加
|
|
26
|
+
* Hex中心のカテゴリ割合Pie Chart描画
|
|
27
|
+
|
|
18
28
|
### v0.0.17 → v0.0.18
|
|
19
29
|
|
|
20
30
|
* **[可視化モジュール]** ```--vis-heat```追加
|
|
31
|
+
* ヒートマップの生成
|
|
21
32
|
|
|
22
33
|
### v0.0.13 → v0.0.14 → v0.0.15 → v0.0.16 → v0.0.17
|
|
23
34
|
* **[可視化モジュール]** ```--vis-majority-hex```追加
|
|
@@ -76,8 +87,12 @@ Visualization (最低一つの指定が必須です)
|
|
|
76
87
|
--vis-majority-hex HexGrid内で最も出現頻度が高いカテゴリの色で彩色。Hex
|
|
77
88
|
apartiteモードで6分割パイチャート表示。透明度は全体
|
|
78
89
|
で正規化。 [真偽] [デフォルト: false]
|
|
90
|
+
--vis-pie-charts Hex中心にカテゴリ割合Pie Chartを表示。半径はグローバル
|
|
91
|
+
出現比に応じてカテゴリ毎に可変化。 [真偽] [デフォルト: false]
|
|
79
92
|
--vis-marker-cluster マーカークラスターとして地図上に表示
|
|
80
93
|
[真偽] [デフォルト: false]
|
|
94
|
+
--vis-voronoi Hex GridごとにVoronoiセルを生成して表示
|
|
95
|
+
[真偽] [デフォルト: false]
|
|
81
96
|
|
|
82
97
|
For bulky Visualizer
|
|
83
98
|
--v-bulky-Radius Point Markerの半径 [数値] [デフォルト: 5]
|
|
@@ -165,6 +180,8 @@ APIキーは以下の3種類の方法で与える事ができます
|
|
|
165
180
|
|
|
166
181
|
### Bulky: 全ての点を地図上にポイントする
|
|
167
182
|
|
|
183
|
+
全ての点を地図上に表示する。
|
|
184
|
+
|
|
168
185
|

|
|
169
186
|
|
|
170
187
|
#### コマンド例
|
|
@@ -186,6 +203,9 @@ $ npx -y -p splatone@latest crawler -p flickr -k "sea,ocean|mountain,mount" --vi
|
|
|
186
203
|
|
|
187
204
|
|
|
188
205
|
### Marker Cluster: 高密度の地点はマーカーをまとめて表示する
|
|
206
|
+
|
|
207
|
+
全マーカーを表示すると、地図上がマーカーで埋め尽くされる問題に対して、高密度地点のマーカー群を一つにまとめてマーカーとする手法。ズームレベルに応じて自動的にマーカーが集約される。
|
|
208
|
+
|
|
189
209
|

|
|
190
210
|
|
|
191
211
|
#### コマンド例
|
|
@@ -201,6 +221,8 @@ $ npx -y -p splatone@latest crawler -p flickr -k "水域=canal,channel,waterway,
|
|
|
201
221
|
|
|
202
222
|
### Heat: ヒートマップ
|
|
203
223
|
|
|
224
|
+
出現頻度に基づいて点の影響範囲をガウス分布で定め連続的に彩色するヒートマップ。
|
|
225
|
+
|
|
204
226
|

|
|
205
227
|
|
|
206
228
|
#### コマンド例
|
|
@@ -208,7 +230,7 @@ $ npx -y -p splatone@latest crawler -p flickr -k "水域=canal,channel,waterway,
|
|
|
208
230
|
* クエリは水域・緑地・交通・ランドマークを色分けしたもの。上記スクリーンショットはフロリダ半島全体
|
|
209
231
|
|
|
210
232
|
```shell
|
|
211
|
-
$ npx -y -p splatone@latest crawler -p flickr -k "canal,river
|
|
233
|
+
$ npx -y -p splatone@latest crawler -p flickr -k "水域#0947ff=canal,river,sea,strait,channel,waterway|交通#00a73d=road,street,alley,sidewalk,bridge|宗教施設#ffb724=chapel,church,cathedral,temple,shrine" --vis-heat --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
212
234
|
```
|
|
213
235
|
|
|
214
236
|
#### コマンドライン引数
|
|
@@ -225,8 +247,10 @@ $ npx -y -p splatone@latest crawler -p flickr -k "canal,river|street,alley|bridg
|
|
|
225
247
|

|
|
226
248
|
|
|
227
249
|
#### コマンド例
|
|
250
|
+
|
|
228
251
|
* クエリは水域・緑地・交通・ランドマークを色分けしたもの。上記スクリーンショットはフロリダ半島全体
|
|
229
|
-
|
|
252
|
+
|
|
253
|
+
|
|
230
254
|
```shell
|
|
231
255
|
$ npx -y -p splatone@latest crawler -p flickr -k "水域=canal,channel,waterway,river,stream,watercourse,sea,ocean,gulf,bay,strait,lagoon,offshore|緑地=forest,woods,turf,lawn,jungle,trees,rainforest,grove,savanna,steppe|交通=bridge,overpass,flyover,aqueduct,trestle,street,road,thoroughfare,roadway,avenue,boulevard,lane,alley,roadway,carriageway,highway,motorway|ランドマーク=church,chapel,cathedral,basilica,minster,temple,shrine,neon,theater,statue,museum,sculpture,zoo,aquarium,observatory" --vis-majority-hex --v-majority-hex-Hexapartite --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
232
256
|
```
|
|
@@ -243,6 +267,56 @@ $ npx -y -p splatone@latest crawler -p flickr -k "水域=canal,channel,waterway,
|
|
|
243
267
|
|
|
244
268
|
* ```--v-majority-hex-Hexapartite```を指定すると各Hexセルを六分割の荒いPie Chartとして中のカテゴリ頻度に応じて彩色します。
|
|
245
269
|
|
|
270
|
+
### Pie Charts: Hex中心にカテゴリ割合Pie Chartを描画
|
|
271
|
+
|
|
272
|
+

|
|
273
|
+
|
|
274
|
+
Hexセル中心に、カテゴリ比率を角度で、グローバル出現数を半径で示すPie Chartを描画します。カテゴリごとに円弧の半径が異なるため、同じHex内でも「世界的にどのカテゴリが多く集まったか」を直感的に比較できます。Pie Chart自体はHex境界内に収まるよう中央へ配置されます。
|
|
275
|
+
|
|
276
|
+
ズームイン/アウト時にはLeafletのzoomイベントをフックしてPie Chartを再描画し、現在の縮尺でもHex境界にフィットする半径が自動再計算されます。
|
|
277
|
+
|
|
278
|
+
#### コマンド例
|
|
279
|
+
|
|
280
|
+
* クエリは水域・交通・宗教施設・緑地を色分け。Hexサイズに応じて自動計算される最大半径を90%まで、最小半径をその40%に設定しています。
|
|
281
|
+
|
|
282
|
+
```shell
|
|
283
|
+
$ npx -y -p splatone@latest crawler -p flickr -k "水域#0947ff=canal,river,sea,strait,channel,waterway,pond|交通#aaaaaa=road,street,alley,sidewalk,bridge|宗教施設#ffb724=chapel,church,cathedral,temple,shrine|緑地#00a73d=forest,woods,trees,mountain,garden,turf" --vis-pie-charts --v-pie-charts-MaxRadiusScale=0.9 --v-pie-charts-MinRadiusScale=0.4 --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### コマンドライン引数
|
|
287
|
+
|
|
288
|
+
| オプション | 説明 | 型 | デフォルト |
|
|
289
|
+
| :--------------------------------------- | :------------------------------------------------------------------------------------------------------------ | :--- | :--------- |
|
|
290
|
+
| `--v-pie-charts-MaxRadiusScale` | Hex内接円半径に対する最大Pie半径の倍率(0-1.5)。1.0でHex境界いっぱい、0.9なら10%余白。 | 数値 | 0.9 |
|
|
291
|
+
| `--v-pie-charts-MinRadiusScale` | 最大半径に対する最小Pie半径の倍率(0-1)。カテゴリが存在する場合に確保する下限割合。 | 数値 | 0.25 |
|
|
292
|
+
| `--v-pie-charts-StrokeWidth` | Pie Chart外周・扇形境界の線幅(px)。 | 数値 | 1 |
|
|
293
|
+
| `--v-pie-charts-BackgroundOpacity` | 最大半径ガイドリングの塗り透明度(0-1)。背景リングの見え方を調整します。 | 数値 | 0.2 |
|
|
294
|
+
|
|
295
|
+
Pie Chartの最大・最小半径は各Hexのジオメトリから算出した内接円半径に基づき動的に決まり、カテゴリごとの扇形半径は「そのHex内カテゴリ出現数 ÷ 全カテゴリ総数」に比例して拡大します。グローバル最大カテゴリのシェアを1として正規化するため、Hex間でもカテゴリ規模を比較できます。
|
|
296
|
+
|
|
297
|
+
### Voronoi: Hex Gridをベースにしたボロノイ分割
|
|
298
|
+
|
|
299
|
+
Hex Gridで集約した各セル内のジオタグを種点としてVoronoi分割を行い、生成したポリゴンをHex境界でクリップして表示します。カテゴリカラーと総数はHex集計結果に基づき、最小間隔/最大サイト数の制御で過密な地域も読みやすく整列できます。
|
|
300
|
+
|
|
301
|
+

|
|
302
|
+
|
|
303
|
+
#### コマンド例
|
|
304
|
+
|
|
305
|
+
* クエリは水域・交通・宗教施設・緑地を色分けしたもの。Hex単位で50m以上離れたサイトだけをVoronoiセルとして採用します。上記の例は東京を範囲としたもの。皇居の緑地や墨田川の水域がよく現れている。
|
|
306
|
+
|
|
307
|
+
```shell
|
|
308
|
+
$ npx -y -p splatone@latest crawler -p flickr -k "水域#0947ff=canal,river,sea,strait,channel,waterway,pond|交通#aaaaaa=road,street,alley,sidewalk,bridge|宗教施設#ffb724=chapel,church,cathedral,temple,shrine|緑地#00a73d=forest,woods,trees,mountain,garden,turf" --vis-voronoi --v-voronoi-MinSiteSpacingMeters=50 --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### コマンドライン引数
|
|
312
|
+
|
|
313
|
+
| オプション | 説明 | 型 | デフォルト |
|
|
314
|
+
| :---------------------------------------- | :---------------------------------------------------------------------------------------- | :--- | :--------- |
|
|
315
|
+
| `--v-voronoi-MaxSitesPerHex` | 1 HexあたりにPoissonサンプリングで残す最大サイト数。0のときは制限なし。 | 数値 | 0 |
|
|
316
|
+
| `--v-voronoi-MinSiteSpacingMeters` | Hex内の採用サイト間で確保する最小距離 (メートル)。ジオタグが密集していても空間的に均等化しつつ、MinSiteSpacingMeters範囲内で出現数の多いカテゴリを優先して残す。 | 数値 | 50 |
|
|
317
|
+
|
|
318
|
+
MinSiteSpacingMetersによる間引きは、各サイト周辺 (MinSiteSpacingMeters以内) の同カテゴリ出現数を優先度として利用するため、同距離内で競合した場合も局所的に密度の高いカテゴリのサイトが採用されやすくなります。一方で密度は低いが他の場所に比べて顕著に出現するカテゴリを見逃す可能性があります。なお、Voronoi図の作成は消費メモリが大きい為、デフォルトでは50m間隔に間引きます。厳密解が必要な場合は```--v-voronoi-MinSiteSpacingMeters=0```を指定してください。ただし、その場合はヒープを使い果たしてクラッシュする可能性があります。マシンパワーに余裕がある場合は```npx --node-options='--max-old-space-size=10240'```のようにヒープサイズを拡大して実行する事も可能です。もう一つのオプション```--v-voronoi-MaxSitesPerHex```はHex内の最大アイテム数を制限するものです。ポワソンサンプリングに基づいてアイテムを間引きます。MinSiteSpacingMetersと共に、適切な結果が得られるよう調整してください。
|
|
319
|
+
|
|
246
320
|
## キーワード指定方法
|
|
247
321
|
|
|
248
322
|
### 比較キーワードの指定
|
|
Binary file
|
|
Binary file
|
package/crawler.js
CHANGED
|
@@ -43,8 +43,8 @@ let pluginsOptions = {};
|
|
|
43
43
|
let visOptions = {};
|
|
44
44
|
|
|
45
45
|
const flickrLimiter = new Bottleneck({
|
|
46
|
-
maxConcurrent:
|
|
47
|
-
minTime:
|
|
46
|
+
maxConcurrent: 6,
|
|
47
|
+
minTime: 700,
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
try {
|
|
@@ -260,10 +260,30 @@ try {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
function concatFC(fc1, fc2) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
...
|
|
266
|
-
|
|
263
|
+
const hasSource = Array.isArray(fc2?.features) && fc2.features.length > 0;
|
|
264
|
+
if (!fc1 || !Array.isArray(fc1.features)) {
|
|
265
|
+
return hasSource ? featureCollection([...fc2.features]) : featureCollection([]);
|
|
266
|
+
}
|
|
267
|
+
if (!hasSource) {
|
|
268
|
+
return fc1;
|
|
269
|
+
}
|
|
270
|
+
fc1.features.push(...fc2.features);
|
|
271
|
+
return fc1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function formatBytesToMB(bytes) {
|
|
275
|
+
return (bytes / (1024 * 1024)).toFixed(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function logHeapUsage(label = 'heap') {
|
|
279
|
+
try {
|
|
280
|
+
const { rss, heapUsed, heapTotal, external } = process.memoryUsage();
|
|
281
|
+
console.log(
|
|
282
|
+
`[memory] ${label}: heapUsed=${formatBytesToMB(heapUsed)} MB / heapTotal=${formatBytesToMB(heapTotal)} MB / rss=${formatBytesToMB(rss)} MB / external=${formatBytesToMB(external)} MB`
|
|
283
|
+
);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.warn('[memory] Failed to read usage stats:', err?.message || err);
|
|
286
|
+
}
|
|
267
287
|
}
|
|
268
288
|
/**
|
|
269
289
|
* /api/hexgrid
|
|
@@ -304,11 +324,14 @@ try {
|
|
|
304
324
|
crawlers[sessionId] = {};
|
|
305
325
|
socket.join(sessionId);
|
|
306
326
|
|
|
327
|
+
const disposeSession = () => {
|
|
328
|
+
delete crawlers[sessionId];
|
|
329
|
+
delete targets[sessionId];
|
|
330
|
+
delete processing[sessionId];
|
|
331
|
+
};
|
|
332
|
+
|
|
307
333
|
socket.on("disconnecting", () => {
|
|
308
|
-
|
|
309
|
-
//console.log("delete session:", socket.rooms);
|
|
310
|
-
delete crawlers[socket.rooms];
|
|
311
|
-
}
|
|
334
|
+
disposeSession();
|
|
312
335
|
//console.log("disconnected:", socket.id);
|
|
313
336
|
});
|
|
314
337
|
|
|
@@ -497,7 +520,7 @@ try {
|
|
|
497
520
|
}
|
|
498
521
|
|
|
499
522
|
// 交差隣接(共有辺で、かつ他Hexの三角形)を付与
|
|
500
|
-
|
|
523
|
+
const triIndex = new Map(triFeatures.map(t => [t.properties.triangleId, t]));
|
|
501
524
|
for (const list of edgeToTriangles.values()) {
|
|
502
525
|
if (list.length < 2) continue; // 共有していなければ隣接なし
|
|
503
526
|
// 同じ辺を共有する全三角形同士で、異なるHexのものを相互に登録
|
|
@@ -525,7 +548,13 @@ try {
|
|
|
525
548
|
|
|
526
549
|
//console.log(JSON.stringify(hexFC, null, 2));
|
|
527
550
|
//res.json({ hex: hexFC, triangles: trianglesFC });
|
|
528
|
-
|
|
551
|
+
const inflight = processing[sessionId] ?? 0;
|
|
552
|
+
if (inflight > 0) {
|
|
553
|
+
console.warn(`[session ${sessionId}] target reinitialized with ${inflight} tasks still running. Resetting session state.`);
|
|
554
|
+
}
|
|
555
|
+
crawlers[sessionId] = {};
|
|
556
|
+
processing[sessionId] = 0;
|
|
557
|
+
targets[sessionId] = { sessionId, hex: hexFC, triangles: trianglesFC, categories, splatonePalette };
|
|
529
558
|
socket.emit("hexgrid", { hex: hexFC, triangles: trianglesFC });
|
|
530
559
|
} catch (e) {
|
|
531
560
|
console.error(e);
|
|
@@ -553,81 +582,117 @@ try {
|
|
|
553
582
|
return resolvedWorkerFilename[taskName];
|
|
554
583
|
}
|
|
555
584
|
|
|
556
|
-
const statsItems = (crawler, target
|
|
557
|
-
const stats = [];
|
|
558
|
-
const total = [];
|
|
559
|
-
const crawled = [];
|
|
585
|
+
const statsItems = (crawler, target) => {
|
|
560
586
|
const progress = [];
|
|
561
|
-
|
|
587
|
+
const categoryCount = Object.keys(target?.categories ?? {}).length;
|
|
588
|
+
const hexCount = target?.hex?.features?.length ?? 0;
|
|
589
|
+
let remaining = categoryCount * hexCount;
|
|
590
|
+
|
|
562
591
|
for (const [hexId, tagsObj] of Object.entries(crawler)) {
|
|
563
|
-
total
|
|
564
|
-
crawled
|
|
565
|
-
for (const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
crawled[hexId] += items.crawled;
|
|
569
|
-
for (const item of items.items.features) {
|
|
570
|
-
//console.log(item.properties)
|
|
571
|
-
stats[hexId] ??= [];
|
|
572
|
-
stats[hexId][item.properties.splatone_triId] ??= [];
|
|
573
|
-
stats[hexId][item.properties.splatone_triId][category] ??= 0;
|
|
574
|
-
stats[hexId][item.properties.splatone_triId][category] += 1;
|
|
592
|
+
let total = 0;
|
|
593
|
+
let crawled = 0;
|
|
594
|
+
for (const items of Object.values(tagsObj)) {
|
|
595
|
+
if (items?.final === true && remaining > 0) {
|
|
596
|
+
remaining--;
|
|
575
597
|
}
|
|
598
|
+
total += Number(items?.total) || 0;
|
|
599
|
+
crawled += Number(items?.crawled) || 0;
|
|
576
600
|
}
|
|
601
|
+
const safeTotal = Math.max(1, total);
|
|
577
602
|
progress[hexId] = {
|
|
578
|
-
percent: total
|
|
579
|
-
crawled
|
|
580
|
-
total
|
|
603
|
+
percent: total === 0 ? 1 : Math.min(1, crawled / safeTotal),
|
|
604
|
+
crawled,
|
|
605
|
+
total
|
|
581
606
|
};
|
|
582
607
|
}
|
|
583
|
-
|
|
584
|
-
return { stats, progress, finish: (finish == 0) };
|
|
608
|
+
return { progress, finish: remaining === 0 };
|
|
585
609
|
};
|
|
586
610
|
|
|
587
611
|
async function runTask_(taskName, data) {
|
|
588
612
|
const { port1, port2 } = new MessageChannel();
|
|
589
613
|
const filename = resolveWorkerFilename(taskName); // ← file URL (href)
|
|
614
|
+
const workerContext = data;
|
|
590
615
|
// named export を呼ぶ場合は { name: "関数名" } を追加
|
|
591
616
|
port1.on('message', (workerResults) => {
|
|
592
617
|
// ここでログ/WebSocket通知/DB書き込みなど何でもOK
|
|
593
|
-
const rtn = workerResults
|
|
594
|
-
|
|
618
|
+
const rtn = workerResults?.results;
|
|
619
|
+
if (!rtn) {
|
|
620
|
+
console.warn('[splatone:start] Received malformed worker payload, skipping chunk.');
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (!workerContext) {
|
|
624
|
+
console.warn('[splatone:start] Missing worker context, skipping chunk.');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const workerOptions = workerContext;
|
|
595
628
|
const currentSessionId = workerOptions.sessionId;
|
|
596
|
-
processing[currentSessionId]
|
|
629
|
+
const currentProcessing = (processing[currentSessionId] ?? 0) - 1;
|
|
630
|
+
processing[currentSessionId] = Math.max(0, currentProcessing);
|
|
631
|
+
const sessionCrawler = crawlers[currentSessionId];
|
|
632
|
+
if (!sessionCrawler) {
|
|
633
|
+
if (argv.debugVerbose) {
|
|
634
|
+
console.warn(`[session ${currentSessionId}] Received worker result after session disposal. Dropping chunk.`);
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (rtn.error) {
|
|
639
|
+
console.warn(`[worker error] hex=${rtn.hexId} category=${rtn.category} code=${rtn.error.code ?? 'n/a'} message=${rtn.error.message ?? 'unknown'}`);
|
|
640
|
+
}
|
|
597
641
|
//console.log(rtn);
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
642
|
+
sessionCrawler[rtn.hexId] ??= {};
|
|
643
|
+
sessionCrawler[rtn.hexId][rtn.category] ??= { items: featureCollection([]) };
|
|
644
|
+
sessionCrawler[rtn.hexId][rtn.category].ids ??= new Set();
|
|
645
|
+
const idSet = sessionCrawler[rtn.hexId][rtn.category].ids;
|
|
646
|
+
|
|
647
|
+
let duplicateCount = 0;
|
|
648
|
+
const uniqueFeatures = [];
|
|
649
|
+
for (const feature of rtn.photos.features) {
|
|
650
|
+
const featureId = feature?.properties?.id;
|
|
651
|
+
if (featureId != null && idSet.has(featureId)) {
|
|
652
|
+
duplicateCount++;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (featureId != null) {
|
|
656
|
+
idSet.add(featureId);
|
|
657
|
+
}
|
|
658
|
+
uniqueFeatures.push(feature);
|
|
659
|
+
}
|
|
660
|
+
sessionCrawler[rtn.hexId][rtn.category].final = rtn.final;
|
|
661
|
+
sessionCrawler[rtn.hexId][rtn.category].crawled ??= 0;
|
|
662
|
+
sessionCrawler[rtn.hexId][rtn.category].total = rtn.final ? sessionCrawler[rtn.hexId][rtn.category].ids.size : rtn.total + sessionCrawler[rtn.hexId][rtn.category].crawled;
|
|
663
|
+
sessionCrawler[rtn.hexId][rtn.category].crawled = sessionCrawler[rtn.hexId][rtn.category].ids.size;
|
|
664
|
+
|
|
665
|
+
if (rtn.photos.features.length >= 250 && duplicateCount === rtn.photos.features.length) {
|
|
609
666
|
console.error("ALL DUPLICATE");
|
|
610
667
|
}
|
|
611
668
|
if (argv.debugVerbose) {
|
|
612
|
-
console.log('INFO:', ` ${rtn.hexId} ${rtn.category} ] dup=${
|
|
669
|
+
console.log('INFO:', ` ${rtn.hexId} ${rtn.category} ] dup=${duplicateCount}, out=${rtn.outside}, in=${rtn.photos.features.length} || ${sessionCrawler[rtn.hexId][rtn.category].crawled} / ${sessionCrawler[rtn.hexId][rtn.category].total}`);
|
|
613
670
|
}
|
|
614
|
-
const photos = featureCollection(
|
|
615
|
-
|
|
616
|
-
= concatFC(
|
|
671
|
+
const photos = featureCollection(uniqueFeatures);
|
|
672
|
+
sessionCrawler[rtn.hexId][rtn.category].items
|
|
673
|
+
= concatFC(sessionCrawler[rtn.hexId][rtn.category].items, photos);
|
|
617
674
|
|
|
618
|
-
const {
|
|
675
|
+
const { progress, finish } = statsItems(sessionCrawler, targets[currentSessionId]);
|
|
619
676
|
io.to(currentSessionId).emit('progress', { hexId: rtn.hexId, progress });
|
|
620
677
|
if (!rtn.final) {
|
|
621
678
|
// 次回クロール用に更新
|
|
622
679
|
rtn.nextPluginOptions.forEach((nextPluginOptions) => {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
680
|
+
const workerOptionsClone = {
|
|
681
|
+
plugin: workerOptions.plugin,
|
|
682
|
+
hex: workerOptions.hex,
|
|
683
|
+
triangles: workerOptions.triangles,
|
|
684
|
+
bbox: workerOptions.bbox,
|
|
685
|
+
category: workerOptions.category,
|
|
686
|
+
tags: workerOptions.tags,
|
|
687
|
+
pluginOptions: nextPluginOptions,
|
|
688
|
+
sessionId: workerOptions.sessionId
|
|
689
|
+
};
|
|
690
|
+
api.emit('splatone:start', workerOptionsClone);
|
|
626
691
|
});
|
|
627
692
|
//} else if (finish) {
|
|
628
693
|
} else if (processing[currentSessionId] == 0) {
|
|
629
694
|
if (argv.debugVerbose) {
|
|
630
|
-
console.table(
|
|
695
|
+
console.table(progress);
|
|
631
696
|
}
|
|
632
697
|
api.emit('splatone:finish', workerOptions);
|
|
633
698
|
}
|
|
@@ -649,11 +714,16 @@ try {
|
|
|
649
714
|
//console.log('[splatone:start]', workerOptions);
|
|
650
715
|
const currentSessionId = workerOptions.sessionId;
|
|
651
716
|
processing[currentSessionId] = (processing[currentSessionId] ?? 0) + 1;
|
|
652
|
-
|
|
717
|
+
const safeOptions = JSON.parse(JSON.stringify(workerOptions, (_, value) => {
|
|
718
|
+
if (typeof value === 'function') return undefined;
|
|
719
|
+
return value;
|
|
720
|
+
}));
|
|
721
|
+
runTask(safeOptions.plugin, safeOptions);
|
|
653
722
|
});
|
|
654
723
|
|
|
655
724
|
await subscribe('splatone:finish', async workerOptions => {
|
|
656
725
|
const currentSessionId = workerOptions.sessionId;
|
|
726
|
+
logHeapUsage(`after-crawl session=${currentSessionId}`);
|
|
657
727
|
const resultId = uniqid();
|
|
658
728
|
const result = crawlers[currentSessionId];
|
|
659
729
|
const target = targets[currentSessionId];
|
package/package.json
CHANGED
package/plugins/flickr/worker.js
CHANGED
|
@@ -3,6 +3,38 @@ import { point, featureCollection } from "@turf/helpers";
|
|
|
3
3
|
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
|
|
4
4
|
import { toUnixSeconds } from '#lib/splatone';
|
|
5
5
|
|
|
6
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
|
|
8
|
+
async function fetchWithRetry(fetcher, { maxAttempts = 4, baseDelayMs = 500 } = {}) {
|
|
9
|
+
let attempt = 0;
|
|
10
|
+
let lastError = null;
|
|
11
|
+
while (attempt < maxAttempts) {
|
|
12
|
+
try {
|
|
13
|
+
return await fetcher();
|
|
14
|
+
} catch (err) {
|
|
15
|
+
lastError = err;
|
|
16
|
+
const transient = isTransientNetworkError(err);
|
|
17
|
+
attempt++;
|
|
18
|
+
if (!transient || attempt >= maxAttempts) {
|
|
19
|
+
throw lastError;
|
|
20
|
+
}
|
|
21
|
+
const waitMs = baseDelayMs * Math.pow(2, attempt - 1);
|
|
22
|
+
console.warn(`[flickr worker] fetch attempt ${attempt} failed (${err?.cause?.code || err?.message}). Retrying in ${waitMs}ms.`);
|
|
23
|
+
await delay(waitMs);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw lastError ?? new Error('Unknown Flickr fetch error');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isTransientNetworkError(err) {
|
|
30
|
+
const code = err?.cause?.code ?? err?.code;
|
|
31
|
+
if (!code && typeof err?.message === 'string' && err.message.includes('fetch failed')) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const transientCodes = new Set(['ECONNABORTED', 'ECONNRESET', 'ETIMEDOUT', 'EPIPE']);
|
|
35
|
+
return transientCodes.has(code);
|
|
36
|
+
}
|
|
37
|
+
|
|
6
38
|
export default async function ({
|
|
7
39
|
port,
|
|
8
40
|
debugVerbose,
|
|
@@ -16,27 +48,33 @@ export default async function ({
|
|
|
16
48
|
sessionId
|
|
17
49
|
}) {
|
|
18
50
|
debugVerbose = true;
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
pluginOptions.TermId = 'a';
|
|
24
|
-
}
|
|
25
|
-
const baseParams = {
|
|
26
|
-
bbox: bbox.join(','),
|
|
27
|
-
tags: tags,
|
|
28
|
-
extras: pluginOptions["Extras"],
|
|
29
|
-
sort: pluginOptions["DateMode"] == "upload" ? "date-posted-desc" : "date-taken-desc"
|
|
51
|
+
|
|
52
|
+
const respond = (payload) => {
|
|
53
|
+
const safePayload = JSON.parse(JSON.stringify(payload));
|
|
54
|
+
port.postMessage(safePayload);
|
|
30
55
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const { flickr } = createFlickr(pluginOptions["APIKEY"]);
|
|
59
|
+
if (!pluginOptions.TermId) {
|
|
60
|
+
//初期TermId
|
|
61
|
+
pluginOptions.TermId = 'a';
|
|
62
|
+
}
|
|
63
|
+
const baseParams = {
|
|
64
|
+
bbox: bbox.join(','),
|
|
65
|
+
tags: tags,
|
|
66
|
+
extras: pluginOptions["Extras"],
|
|
67
|
+
sort: pluginOptions["DateMode"] == "upload" ? "date-posted-desc" : "date-taken-desc"
|
|
68
|
+
};
|
|
69
|
+
baseParams[pluginOptions["DateMode"] == "upload" ? 'max_upload_date' : 'max_taken_date'] = pluginOptions["DateMax"];
|
|
70
|
+
baseParams[pluginOptions["DateMode"] == "upload" ? 'min_upload_date' : 'min_taken_date'] = pluginOptions["DateMin"];
|
|
71
|
+
//console.log("[baseParams]",baseParams);
|
|
72
|
+
const res = await fetchWithRetry(() => flickr("flickr.photos.search", {
|
|
73
|
+
...baseParams,
|
|
74
|
+
has_geo: 1,
|
|
75
|
+
per_page: 250,
|
|
76
|
+
page: 1,
|
|
77
|
+
}));
|
|
40
78
|
//console.log(res);
|
|
41
79
|
const ids = [];
|
|
42
80
|
const authors = {};
|
|
@@ -127,30 +165,49 @@ export default async function ({
|
|
|
127
165
|
}
|
|
128
166
|
}
|
|
129
167
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
168
|
+
const payload = {
|
|
169
|
+
results: {
|
|
170
|
+
photos,
|
|
171
|
+
hexId: hex.properties.hexId,
|
|
172
|
+
tags,
|
|
173
|
+
category,
|
|
174
|
+
nextPluginOptions: nextPluginOptionsDelta.map(e => { return { ...pluginOptions, ...e } }),
|
|
175
|
+
total: res.photos.total,
|
|
176
|
+
outside: outside,
|
|
177
|
+
ids,
|
|
178
|
+
final: nextPluginOptionsDelta.length == 0//res.photos.photo.length == res.photos.total
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
respond(payload);
|
|
183
|
+
return true;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error('[flickr worker] Fatal error', {
|
|
139
186
|
sessionId,
|
|
140
|
-
|
|
141
|
-
results: {
|
|
142
|
-
photos,
|
|
143
|
-
hexId: hex.properties.hexId,
|
|
144
|
-
tags,
|
|
187
|
+
hexId: hex?.properties?.hexId,
|
|
145
188
|
category,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
189
|
+
reason: err?.message,
|
|
190
|
+
code: err?.cause?.code || err?.code || null
|
|
191
|
+
});
|
|
192
|
+
respond({
|
|
193
|
+
results: {
|
|
194
|
+
photos: featureCollection([]),
|
|
195
|
+
hexId: hex?.properties?.hexId ?? null,
|
|
196
|
+
tags,
|
|
197
|
+
category,
|
|
198
|
+
nextPluginOptions: [],
|
|
199
|
+
total: 0,
|
|
200
|
+
outside: 0,
|
|
201
|
+
ids: [],
|
|
202
|
+
final: true,
|
|
203
|
+
error: {
|
|
204
|
+
message: err?.message,
|
|
205
|
+
code: err?.cause?.code || err?.code || null
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
154
211
|
return {
|
|
155
212
|
photos,
|
|
156
213
|
hexId: hex.properties.hexId,
|