splatone 0.0.32 → 0.0.33

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
@@ -4,12 +4,12 @@
4
4
  - [Splatone - Multi-layer Composite Heatmap](#splatone---multi-layer-composite-heatmap)
5
5
  - [概要](#概要)
6
6
  - [Change Log](#change-log)
7
+ - [v0.0.22 → v0.0.33](#v0022--v0033)
7
8
  - [v0.0.29 → → v0.0.32](#v0029---v0032)
8
9
  - [v0.0.28 → v0.0.29](#v0028--v0029)
9
10
  - [v0.0.23 → → v0.0.28](#v0023--v0028)
10
11
  - [v0.0.22 → → v0.0.23](#v0022--v0023)
11
12
  - [使い方](#使い方)
12
- - [Helpの表示](#helpの表示)
13
13
  - [最小コマンド例](#最小コマンド例)
14
14
  - [ブラウズ専用モード](#ブラウズ専用モード)
15
15
  - [インタラクティブモード](#インタラクティブモード)
@@ -20,6 +20,8 @@
20
20
  - [コマンドライン引数](#コマンドライン引数)
21
21
  - [GimmeGimmeモードで取得する写真とそのファイル名について](#gimmegimmeモードで取得する写真とそのファイル名について)
22
22
  - [Flickr APIキーの与え方](#flickr-apiキーの与え方)
23
+ - [gmap: Google Places Text Searchを取得するクローラー](#gmap-google-places-text-searchを取得するクローラー)
24
+ - [overpass: OpenStreetMapの地点を取得するクローラー](#overpass-openstreetmapの地点を取得するクローラー)
23
25
  - [Visualizer (可視化モジュール)](#visualizer-可視化モジュール)
24
26
  - [Bulky: 全ての点を地図上にポイントする](#bulky-全ての点を地図上にポイントする)
25
27
  - [コマンド例](#コマンド例)
@@ -61,9 +63,11 @@
61
63
 
62
64
  ## <a name=''></a>概要
63
65
 
64
- SNSのジオタグ付きポストをキーワードに基づいて収集するツールです。キーワードは複数指定し、それぞれのキーワードの出現分布を地図上にマップします。現在は以下のSNSに対応しています。
66
+ SNSのジオタグ付きポストをキーワードに基づいて収集するツールです。キーワードは複数指定し、それぞれのキーワードの出現分布を地図上にマップします。現在は以下のソースに対応しています。
65
67
 
66
- - Flickr
68
+ - Flickr (provider名: flickr)
69
+ - Google Places Text Search (provider名: gmap)
70
+ - Overpass API / OpenStreetMap (provider名: overpass)
67
71
 
68
72
  集めたデータはキーワード毎に色分けされ地図上で可視化されます。以下の可視化手法に対応しています。
69
73
 
@@ -79,10 +83,17 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
79
83
 
80
84
  ## <a name='ChangeLog'></a>Change Log
81
85
 
86
+ ### <a name='v0.0.29v0.0.32'></a>v0.0.22 → v0.0.33
87
+
88
+ * Google Maps Place APIからvenueをクローリングするProviderを実装: ```-p gmap```
89
+ * OpenStreetMap Overpass APIからvenueをクローリングするProviderを実装: ```-p overpass```
90
+
82
91
  ### <a name='v0.0.29v0.0.32'></a>v0.0.29 → → v0.0.32
83
92
 
84
93
  * BrowseモードにURL読み込み機能(デモモード)追加
85
94
  * GitHub上に東京タワーとスカイツリーを例としてすべての可視化結果を掲載
95
+ * gmapプロバイダ追加: Google Places Text Search APIから地点を取得
96
+ * overpassプロバイダ追加: Overpass APIからOpenStreetMapのPOIを取得
86
97
 
87
98
  ### <a name='v0.0.28v0.0.29'></a>v0.0.28 → v0.0.29
88
99
 
@@ -117,164 +128,6 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
117
128
  - [Node.js](https://nodejs.org/ja/download)をインストール後、npxで実行します。
118
129
  - npxはnpm上のモジュールをコマンド一つでインストールと実行を行う事ができるコマンドです。
119
130
 
120
- ## <a name='Help'></a>Helpの表示
121
-
122
- ```shell
123
- $ npx -y -p splatone@latest crawler --help
124
- [app] [provider] loaded: flickr@1.0.0
125
- 使い方: crawler.js [options]
126
-
127
- Basic Options
128
- -p, --provider 実行するプロバイダ [文字列] [選択してください: "flickr"]
129
- -k, --keywords 検索キーワード(|区切り) [文字列] [デフォルト:
130
- "nature,tree,flower|building,house|water,sea,river,pond"]
131
- -f, --filed 大きなデータをファイルとして送受信する
132
- [真偽] [デフォルト: true]
133
- -c, --chopped 大きなデータを細分化して送受信する
134
- [非推奨] [真偽] [デフォルト: false]
135
- --browse-mode ブラウズ専用モード(範囲描画とクロールを無効化)
136
- [真偽] [デフォルト: false]
137
-
138
- Debug
139
- --debug-verbose デバッグ情報出力 [真偽] [デフォルト: false]
140
-
141
- UI Defaults
142
- --ui-cell-size 起動時にUIへ設定するセルサイズ (0で自動)
143
- [数値] [デフォルト: 0]
144
- --ui-units セルサイズの単位 (kilometers/meters/miles)
145
- [文字列] [選択してください: "kilometers", "meters", "miles"] [デフォルト:
146
- "kilometers"]
147
- --ui-bbox UI初期表示の矩形範囲。"minLon,minLat,maxLon,maxLat" の形式
148
- [文字列]
149
- --ui-polygon UI初期表示のポリゴン。Polygon/MultiPolygonを含むGeoJSON文
150
- 字列 [文字列]
151
- --city 起動時に中心付近を合わせる都市名(例: "Tokyo") [文字列]
152
-
153
- For flickr Provider
154
- --p-flickr-APIKEY Flickr ServiceのAPI KEY [文字列]
155
- --p-flickr-Extras カンマ区切り/保持する写真のメタデータ(デフォルト値
156
- は記載の有無に関わらず保持)
157
- [文字列] [デフォルト: "date_upload,date_taken,owner_name,geo,url_sq,tags"]
158
- --p-flickr-DateMode 利用時間軸(update=Flickr投稿日時/taken=写真撮影日時
159
- )
160
- [選択してください: "upload", "taken"] [デフォルト: "upload"]
161
- --p-flickr-Haste 時間軸分割並列処理 [真偽] [デフォルト: true]
162
- --p-flickr-GimmeGimme Flickr画像を保存するディレクトリパス(指定しない場合
163
- は保存しない) [文字列]
164
- --p-flickr-DateMax クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD
165
- [文字列] [デフォルト: 1764679068]
166
- --p-flickr-DateMin クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD
167
- [文字列] [デフォルト: 1072882800]
168
-
169
- Visualization (最低一つの指定が必須です)
170
- --vis-bulky 全データをCircleMarkerとして地図上に表示
171
- [真偽] [デフォルト: false]
172
- --vis-dbscan クロール結果をDBSCANクラスタリングし、クラスタの凸包
173
- をポリゴンで可視化します。[真偽] [デフォルト: false]
174
- --vis-heat カテゴリ毎に異なるレイヤのヒートマップで可視化(色=
175
- カテゴリ色、透明度=頻度) [真偽] [デフォルト: false]
176
- --vis-majority-hex HexGrid内で最も出現頻度が高いカテゴリの色で彩色。Hex
177
- apartiteモードで6分割パイチャート表示。透明度は全体
178
- で正規化。 [真偽] [デフォルト: false]
179
- --vis-marker-cluster マーカークラスターとして地図上に表示
180
- [真偽] [デフォルト: false]
181
- --vis-pie-charts Hex中心にカテゴリ割合のPie
182
- Chartを描画するビジュアライザ
183
- [真偽] [デフォルト: false]
184
- --vis-voronoi Hex Grid ボロノイ図 [真偽] [デフォルト: false]
185
-
186
- For bulky Visualizer
187
- --v-bulky-Radius Point Markerの半径 | min=0, step=1
188
- [数値] [デフォルト: 5]
189
- --v-bulky-Stroke Point Markerの線の有無 [真偽] [デフォルト: true]
190
- --v-bulky-Weight Point Markerの線の太さ | min=0, step=1
191
- [数値] [デフォルト: 1]
192
- --v-bulky-Opacity Point Markerの線の透明度 | min=0, max=1, step=0.05
193
- [数値] [デフォルト: 1]
194
- --v-bulky-Filling Point Markerの塗りの有無 [真偽] [デフォルト: true]
195
- --v-bulky-FillOpacity Point Markerの塗りの透明度 | min=0, max=1,
196
- step=0.05 [数値] [デフォルト: 0.5]
197
-
198
- For dbscan Visualizer
199
- --v-dbscan-Eps DBSCANのeps(クラスタ判定距離) | min=0.01,
200
- step=0.01 [数値] [デフォルト: 0.6]
201
- --v-dbscan-MinPts DBSCANのminPts(クラスタ確定に必要な点数) |
202
- min=1, step=1 [数値] [デフォルト: 6]
203
- --v-dbscan-Units epsで使用する距離単位
204
- [文字列] [選択してください: "kilometers", "meters", "miles"] [デフォルト:
205
- "kilometers"]
206
- --v-dbscan-StrokeWidth ポリゴン輪郭の太さ | min=0, max=10, step=0.5
207
- [数値] [デフォルト: 2]
208
- --v-dbscan-StrokeOpacity ポリゴン輪郭の透明度 | min=0, max=1, step=0.05
209
- [数値] [デフォルト: 0.9]
210
- --v-dbscan-FillOpacity ポリゴン塗りの透明度 | min=0, max=1, step=0.05
211
- [数値] [デフォルト: 0.35]
212
- --v-dbscan-DashArray LeafletのdashArray指定(例: "4 6") | 例: 例: 4
213
- 6 [文字列] [デフォルト: ""]
214
- --v-dbscan-KernelScale KDEカーネル半径をepsに対して何倍にするか |
215
- min=0.1, max=10, step=0.1[数値] [デフォルト: 1]
216
- --v-dbscan-GridSize KDE計算用グリッド解像度(長辺方向セル数) |
217
- min=8, max=256, step=1 [数値] [デフォルト: 80]
218
- --v-dbscan-ContourPercent 最大密度に対する等値線レベル(0-1) | min=0.05,
219
- max=0.95, step=0.05 [数値] [デフォルト: 0.4]
220
-
221
- For heat Visualizer
222
- --v-heat-Radius ヒートマップブラーの半径(Unitsで指定した距離単
223
- 位) | min=0, step=1 [数値] [デフォルト: 50]
224
- --v-heat-Units Radiusに使用する距離単位
225
- [文字列] [選択してください: "kilometers", "meters", "miles"] [デフォルト:
226
- "meters"]
227
- --v-heat-MinOpacity ヒートマップの最小透明度 | min=0, max=1,
228
- step=0.05 [数値] [デフォルト: 0]
229
- --v-heat-MaxOpacity ヒートマップの最大透明度 | min=0, max=1,
230
- step=0.05 [数値] [デフォルト: 1]
231
- --v-heat-MaxValue ヒートマップ強度の最大値
232
- (未指定時はデータから自動推定) | step=1 [数値]
233
- --v-heat-WeightThreshold 半径内の近傍点数(自分以外)がこの値未満の点は描
234
- 画しない | min=0, step=1 [数値] [デフォルト: 1]
235
-
236
- For majority-hex Visualizer
237
- --v-majority-hex-Hexapartite 中のカテゴリの頻度に応じて六角形を分割色彩
238
- [真偽] [デフォルト: false]
239
- --v-majority-hex-HexOpacity 六角形の線の透明度 | min=0, max=1, step=0.05
240
- [数値] [デフォルト: 1]
241
- --v-majority-hex-HexWeight 六角形の線の太さ | min=0, step=1
242
- [数値] [デフォルト: 1]
243
- --v-majority-hex-MaxOpacity 正規化後の最大塗り透明度 | min=0, max=1,
244
- step=0.05 [数値] [デフォルト: 0.9]
245
- --v-majority-hex-MinOpacity 正規化後の最小塗り透明度 | min=0, max=1,
246
- step=0.05 [数値] [デフォルト: 0.5]
247
-
248
- For marker-cluster Visualizer
249
- --v-marker-cluster-MaxClusterRadius クラスタを構成する範囲(半径) | min=1,
250
- step=1 [数値] [デフォルト: 80]
251
-
252
- For pie-charts Visualizer
253
- --v-pie-charts-MaxRadiusScale Hex内接円半径に対する最大半径スケール
254
- (0-1.5) | min=0.1, max=1.5, step=0.05
255
- [数値] [デフォルト: 0.9]
256
- --v-pie-charts-MinRadiusScale 最大半径に対する最小半径スケール (0-1) |
257
- min=0, max=1, step=0.05
258
- [数値] [デフォルト: 0.25]
259
- --v-pie-charts-StrokeWidth Pie Chart輪郭線の太さ(px) | min=0,
260
- step=1 [数値] [デフォルト: 1]
261
- --v-pie-charts-BackgroundOpacity 最大半径ガイドリングの透明度 (0-1) |
262
- min=0, max=1, step=0.05
263
- [数値] [デフォルト: 0.2]
264
-
265
- For voronoi Visualizer
266
- --v-voronoi-MaxSitesPerHex ポワソン分布に基づいて各ヘックス内でサン
267
- プリングされる最大サイト数 (0 = 無制限)
268
- | min=0, step=1 [数値] [デフォルト: 0]
269
- --v-voronoi-MinSiteSpacingMeters 各ヘックス内でサンプリングされたサイト間
270
- の最小距離をメートル単位で保証 (0 =
271
- 無効) | min=0, step=1
272
- [数値] [デフォルト: 50]
273
-
274
- オプション:
275
- --help ヘルプを表示 [真偽]
276
- --version バージョンを表示 [真偽]
277
- ```
278
131
 
279
132
  ## <a name='-1'></a>最小コマンド例
280
133
 
@@ -347,6 +200,8 @@ npx -y -p splatone@latest browse \
347
200
  | ```--p-flickr-GimmeGimme``` | 取得した画像を保存するディレクトリ(未指定時はダウンロードせず/失敗時は同名txtで記録) | 文字列 | |
348
201
  | ```--p-flickr-DateMax``` | クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD | 文字列 | (動的)現時刻 |
349
202
  | ```--p-flickr-DateMin``` | クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD | 文字列 | 1072882800 |
203
+ | ```--p-flickr-ThrottleMaxConcurrent``` | Flickr API リクエストの同時実行数 | 数値 | 2 |
204
+ | ```--p-flickr-ThrottleMinTimeMs``` | 連続する Flickr リクエスト間の最小待機時間 (ミリ秒) | 数値 | 500 |
350
205
 
351
206
  #### <a name='GimmeGimme'></a>GimmeGimmeモードで取得する写真とそのファイル名について
352
207
  - ```--p-flickr-GimmeGimme=保存ディレクトリのパス```のように指定してください。
@@ -371,6 +226,43 @@ APIキーは以下の3種類の方法で与える事ができます
371
226
  - **flickr**の場合は```.API_KEY.flickr```になります。
372
227
  - optionや環境変数で与えるよりも優先されます。
373
228
 
229
+ ### <a name='gmap-google-places-text-searchを取得するクローラー'></a>gmap: Google Places Text Searchを取得するクローラー
230
+
231
+ Google Maps Places Text Search API を利用して、テキストクエリに合致する POI を Hex 単位で収集します。`-p gmap` を指定し、`--keywords` で `カテゴリ名=TextSearchクエリ` を与えると、カテゴリ名で彩色しながらクエリ文字列を Google に送信します(例: `-k "カフェ=cafe tokyo|観光=landmark tokyo"`)。検索半径は UI のセルサイズ (`--ui-cell-size` と `--ui-units`) をメートル換算した値を自動適用し、API の上限である 50km を超えないようクランプされます。さらに各 Hex の外接 bbox を `locationbias=rectangle` として付与し、Text Search リクエスト自体が対象 Hex の範囲に収束するよう制御しています(最終結果も Hex 内判定でフィルタ)。
232
+
233
+ | オプション | 説明 | 型 | デフォルト |
234
+ | :-- | :-- | :-- | :-- |
235
+ | `--p-gmap-APIKEY` | Google Places API キー。省略時は `.API_KEY.gmap` もしくは `API_KEY_gmap` 環境変数から読み込み。 | 文字列 | |
236
+ | `--p-gmap-Language` | Places API の `language` パラメータ。 | 文字列 | `ja` |
237
+ | `--p-gmap-MaxPages` | Text Search の最大ページ数 (1〜3)。Google 側の仕様上、最大 3 ページ / 60 件。 | 数値 | 3 |
238
+ | `--p-gmap-ThrottleMaxConcurrent` | Google Places リクエストの同時実行本数。 | 数値 | 2 |
239
+ | `--p-gmap-ThrottleMinTimeMs` | 連続する Places リクエスト間の最小待機時間 (ミリ秒)。 | 数値 | 500 |
240
+
241
+ 進捗計算は内部定数 (60 件/Hexカテゴリ) を初期期待値として扱い、`next_page_token` が返らなくなったタイミングで実測件数に合わせて 100% に到達させるハイブリッド方式を用いています。
242
+
243
+ Places API は 2 ページ目以降を取得する際に 2 秒程度の待ち時間が必要なため、内部で自動的に待機してから次ページのジョブを投入します。返却される地点はカテゴリ × Hex 単位で重複除去され、Flickr 由来のデータと同様に全ビジュアライザへそのまま流れます。
244
+
245
+ ### <a name='overpass-openstreetmapの地点を取得するクローラー'></a>overpass: OpenStreetMapの地点を取得するクローラー
246
+
247
+ OpenStreetMap の Overpass API を用いて、タグ条件に一致するノード/ウェイ/リレーションを Hex 単位で収集します。`-p overpass` を指定し、`--keywords` で `カテゴリ名=タグ条件` を与えると、カテゴリ毎に Hex bbox 内へ個別の Overpass クエリを投げます。タグ条件は `amenity=restaurant` のような `key=value` 形式を基本とし、複数指定したい場合は `,` で区切って OR 検索します(例: `-k "麺類#D93C3C=amenity=ramen,amenity=noodle_shop"`)。`node:amenity=cafe` のように `node|way|relation:` プレフィックスを付けると特定の幾何種のみを対象にできます。
248
+
249
+ | オプション | 説明 | 型 | デフォルト |
250
+ | :-- | :-- | :-- | :-- |
251
+ | `--p-overpass-Endpoint` | 利用する Overpass API interpreter の URL。 | 文字列 | `https://overpass-api.de/api/interpreter` |
252
+ | `--p-overpass-TimeoutSeconds` | Overpass への 1 リクエストあたりのタイムアウト秒数。 | 数値 | 25 |
253
+ | `--p-overpass-MaxRetries` | HTTP/ネットワークエラー時にリトライする最大回数。 | 数値 | 3 |
254
+ | `--p-overpass-UserAgent` | Overpass API に送信する User-Agent。連絡先付きの文字列を推奨。 | 文字列 | `Splatone-Overpass (+https://github.com/YokoyamaLab/Splatone)` |
255
+ | `--p-overpass-ThrottleMaxConcurrent` | Overpass への同時リクエスト最大数。推奨は 1。 | 数値 | 1 |
256
+ | `--p-overpass-ThrottleMinTimeMs` | 連続リクエスト間の待ち時間 (ミリ秒)。 | 数値 | 1500 |
257
+
258
+ 進捗は「カテゴリ内に投入したクエリ数」を 100% とみなし、各クエリのレスポンスを受け取るたびに均等配分で加算します。たとえば 2 カテゴリ × 2 条件 (合計 4 クエリ) の場合、1 クエリ完了ごとに Hex 全体の進捗が 25% ずつ前進します。Overpass は共有リソースであるため、大きな bbox や極端に多いクエリを連続実行する際は時間帯やエンドポイント(市民大・Kumi Systems 等)にも配慮してください。デフォルトでは 1 本ずつ 1.5 秒間隔でシリアライズ送信しますが、必要であれば `--p-overpass-Throttle*` オプションで上書きできます(ただし連続アクセスしすぎないよう注意)。
259
+
260
+ ```bash
261
+ $ npx -y -p splatone@latest crawler -p overpass -k "カフェ#ff5f5f=amenity=cafe,amenity=coffee_shop|文化施設#3366ff=tourism=museum,tourism=gallery" --vis-bulky --city "Kyoto"
262
+ ```
263
+
264
+ 上記例では京都市内の Hex を対象に、カフェ系と文化施設系の OpenStreetMap POI を 2 つのカテゴリで色分けしながら取得します。逓減処理後のジオメトリは Leaflet 上のどのビジュアライザでも利用できます。
265
+
374
266
  ## <a name='Visualizer'></a>Visualizer (可視化モジュール)
375
267
 
376
268
  ### <a name='Bulky:'></a>Bulky: 全ての点を地図上にポイントする
package/crawler.js CHANGED
@@ -706,7 +706,8 @@ try {
706
706
  hex: cloneJsonSafe(target.hex ?? null),
707
707
  triangles: cloneJsonSafe(target.triangles ?? null),
708
708
  categories: cloneJsonSafe(target.categories ?? {}),
709
- splatonePalette: cloneJsonSafe(target.splatonePalette ?? {})
709
+ splatonePalette: cloneJsonSafe(target.splatonePalette ?? {}),
710
+ gridMeta: cloneJsonSafe(target.gridMeta ?? null)
710
711
  };
711
712
  }
712
713
 
@@ -769,7 +770,8 @@ try {
769
770
  hex: target.hex ?? context.hexGrid ?? null,
770
771
  triangles: target.triangles ?? context.triangles ?? null,
771
772
  categories: target.categories ?? context.categories ?? {},
772
- splatonePalette: target.splatonePalette ?? payload.palette ?? {}
773
+ splatonePalette: target.splatonePalette ?? payload.palette ?? {},
774
+ gridMeta: target.gridMeta ?? context.gridMeta ?? null
773
775
  };
774
776
  return {
775
777
  version: payload.version ?? 1,
@@ -786,6 +788,7 @@ try {
786
788
  hexGrid: normalizedTarget.hex,
787
789
  triangles: normalizedTarget.triangles,
788
790
  categories: normalizedTarget.categories,
791
+ gridMeta: normalizedTarget.gridMeta ?? null,
789
792
  cliOptions: (payload.context && payload.context.cliOptions) ? payload.context.cliOptions : {}
790
793
  }
791
794
  };
@@ -797,7 +800,8 @@ try {
797
800
  hex: cloneJsonSafe(sourceTarget.hex ?? null),
798
801
  triangles: cloneJsonSafe(sourceTarget.triangles ?? null),
799
802
  categories: cloneJsonSafe(sourceTarget.categories ?? {}),
800
- splatonePalette: cloneJsonSafe(sourceTarget.splatonePalette ?? rawPayload?.palette ?? {})
803
+ splatonePalette: cloneJsonSafe(sourceTarget.splatonePalette ?? rawPayload?.palette ?? {}),
804
+ gridMeta: cloneJsonSafe(sourceTarget.gridMeta ?? null)
801
805
  };
802
806
  }
803
807
 
@@ -1205,7 +1209,8 @@ try {
1205
1209
  triangles: targets[req.sessionId].triangles,
1206
1210
  sessionId: req.sessionId,
1207
1211
  categories: targets[req.sessionId].categories,
1208
- providerOptions: providersOptions[argv.provider]
1212
+ providerOptions: providersOptions[argv.provider],
1213
+ gridMeta: targets[req.sessionId].gridMeta ?? null
1209
1214
  };
1210
1215
  await providers.call(argv.provider, 'crawl', workerOptions);
1211
1216
  }
@@ -1230,6 +1235,10 @@ try {
1230
1235
  return;
1231
1236
  }
1232
1237
  let { bbox, drawn, cellSize = 0, units = 'kilometers', tags = 'sea,beach|mountain,forest' } = req.query;
1238
+ units = typeof units === 'string' ? units.toLowerCase() : 'kilometers';
1239
+ if (!VALID_UI_UNITS.has(units)) {
1240
+ units = 'kilometers';
1241
+ }
1233
1242
  const boundary = String(bbox).split(',').map(Number);
1234
1243
  if (cellSize == 0) {
1235
1244
  //セルサイズ自動決定
@@ -1255,14 +1264,22 @@ try {
1255
1264
 
1256
1265
  if (bbox) {
1257
1266
  if (boundary.length !== 4 || !boundary.every(Number.isFinite)) {
1258
- return res.status(400).json({ error: 'bbox must be "minLon,minLat,maxLon,maxLat"' });
1267
+ socket.emit('toast', {
1268
+ text: 'bbox must be "minLon,minLat,maxLon,maxLat"',
1269
+ class: 'error'
1270
+ });
1271
+ return;
1259
1272
  }
1260
1273
  bboxArray = boundary;
1261
1274
  }
1262
1275
 
1263
1276
  const sizeNum = Number(cellSize);
1264
1277
  if (!Number.isFinite(sizeNum) || sizeNum <= 0) {
1265
- return res.status(400).json({ error: 'cellSize must be a positive number' });
1278
+ socket.emit('toast', {
1279
+ text: 'cellSize must be a positive number',
1280
+ class: 'error'
1281
+ });
1282
+ return;
1266
1283
  }
1267
1284
 
1268
1285
  //カテゴリ生成
@@ -1416,7 +1433,8 @@ try {
1416
1433
  }
1417
1434
  crawlers[sessionId] = {};
1418
1435
  processing[sessionId] = 0;
1419
- targets[sessionId] = { sessionId, hex: hexFC, triangles: trianglesFC, categories, splatonePalette };
1436
+ const gridMeta = { cellSize: sizeNum, units };
1437
+ targets[sessionId] = { sessionId, hex: hexFC, triangles: trianglesFC, categories, splatonePalette, gridMeta };
1420
1438
  socket.emit("hexgrid", { hex: hexFC, triangles: trianglesFC });
1421
1439
  } catch (e) {
1422
1440
  console.error(e);
@@ -1489,6 +1507,7 @@ try {
1489
1507
  triangles: target?.triangles?.features?.length ?? 0,
1490
1508
  categories: Object.keys(target?.categories ?? {}).length,
1491
1509
  crawled: 0,
1510
+ progressCompleted: 0,
1492
1511
  remaining: 0,
1493
1512
  expected: 0,
1494
1513
  percent: 0
@@ -1500,6 +1519,7 @@ try {
1500
1519
  const hexStats = {
1501
1520
  categories: {},
1502
1521
  crawled: 0,
1522
+ progressCompleted: 0,
1503
1523
  remaining: 0,
1504
1524
  expected: 0,
1505
1525
  percent: 0
@@ -1508,34 +1528,44 @@ try {
1508
1528
  const crawled = info?.ids instanceof Set
1509
1529
  ? info.ids.size
1510
1530
  : Number(info?.crawled) || 0;
1511
- const remaining = Number(info?.remaining) || 0;
1512
- const total = Number.isFinite(info?.total)
1513
- ? Number(info.total)
1514
- : crawled + remaining;
1515
- const percent = total === 0 ? 1 : Math.min(1, crawled / Math.max(1, total));
1531
+ const progressCompleted = Number.isFinite(info?.progressCompleted)
1532
+ ? info.progressCompleted
1533
+ : crawled;
1534
+ const expectedTotal = Number.isFinite(info?.progressExpected)
1535
+ ? info.progressExpected
1536
+ : (Number.isFinite(info?.total) ? Number(info.total) : crawled + (Number(info?.remaining) || 0));
1537
+ const progressRemaining = Number.isFinite(info?.progressRemaining)
1538
+ ? Math.max(0, info.progressRemaining)
1539
+ : Math.max(0, expectedTotal - progressCompleted);
1540
+ const percent = expectedTotal === 0 ? 1 : Math.min(1, progressCompleted / Math.max(1, expectedTotal));
1516
1541
  hexStats.categories[categoryName] = {
1517
1542
  crawled,
1518
- remaining,
1519
- total,
1543
+ progressCompleted,
1544
+ remaining: progressRemaining,
1545
+ total: expectedTotal,
1520
1546
  percent,
1521
1547
  final: info?.final === true
1522
1548
  };
1523
1549
  hexStats.crawled += crawled;
1524
- hexStats.remaining += remaining;
1525
- hexStats.expected += total;
1550
+ hexStats.progressCompleted += progressCompleted;
1551
+ hexStats.remaining += progressRemaining;
1552
+ hexStats.expected += expectedTotal;
1526
1553
  }
1554
+ const hexProgressValue = hexStats.progressCompleted || hexStats.crawled;
1527
1555
  hexStats.percent = hexStats.expected === 0
1528
1556
  ? 1
1529
- : Math.min(1, hexStats.crawled / Math.max(1, hexStats.expected));
1557
+ : Math.min(1, hexProgressValue / Math.max(1, hexStats.expected));
1530
1558
  summary.hexes[hexId] = hexStats;
1531
1559
  summary.totals.crawled += hexStats.crawled;
1560
+ summary.totals.progressCompleted += hexProgressValue;
1532
1561
  summary.totals.remaining += hexStats.remaining;
1533
1562
  summary.totals.expected += hexStats.expected;
1534
1563
  }
1535
1564
 
1565
+ const totalProgressValue = summary.totals.progressCompleted || summary.totals.crawled;
1536
1566
  summary.totals.percent = summary.totals.expected === 0
1537
1567
  ? 1
1538
- : Math.min(1, summary.totals.crawled / Math.max(1, summary.totals.expected));
1568
+ : Math.min(1, totalProgressValue / Math.max(1, summary.totals.expected));
1539
1569
 
1540
1570
  return summary;
1541
1571
  }
@@ -1585,16 +1615,14 @@ try {
1585
1615
  }
1586
1616
  sessionCrawler[rtn.hexId] ??= {};
1587
1617
  sessionCrawler[rtn.hexId][rtn.category] ??= {};
1588
- sessionCrawler[rtn.hexId][rtn.category].terms ??= {};
1589
- if (!sessionCrawler[rtn.hexId][rtn.category].terms[rtn.TermId]) {
1590
- //一つ上のTermIdを100%に更新。ラベルはPrefixLabelingなのでrtn.TermId.slice(0,-1)となる。
1591
- const prevTermId = rtn.TermId.slice(0, -1);
1592
- if (sessionCrawler[rtn.hexId][rtn.category].terms[prevTermId] && !sessionCrawler[rtn.hexId][rtn.category].terms[prevTermId].final) {
1593
- sessionCrawler[rtn.hexId][rtn.category].terms[prevTermId].final = true;
1594
- sessionCrawler[rtn.hexId][rtn.category].terms[prevTermId].remaining = 0;
1618
+ const termMap = sessionCrawler[rtn.hexId][rtn.category].terms ??= {};
1619
+ if (rtn.prevTermId && termMap[rtn.prevTermId]) {
1620
+ if (!termMap[rtn.prevTermId].final) {
1621
+ termMap[rtn.prevTermId].final = true;
1595
1622
  }
1623
+ termMap[rtn.prevTermId].remaining = 0;
1596
1624
  }
1597
- sessionCrawler[rtn.hexId][rtn.category].terms[rtn.TermId] ??= {};
1625
+ termMap[rtn.TermId] ??= {};
1598
1626
  sessionCrawler[rtn.hexId][rtn.category].items ??= featureCollection([]);
1599
1627
  sessionCrawler[rtn.hexId][rtn.category].ids ??= new Set();
1600
1628
 
@@ -1615,17 +1643,48 @@ try {
1615
1643
  uniqueFeatures.push(feature);
1616
1644
  }
1617
1645
 
1618
- currentHexCategory.terms[rtn.TermId].remaining = rtn.remaining;
1619
- currentHexCategory.terms[rtn.TermId].final = rtn.final;
1646
+ const termEntry = currentHexCategory.terms[rtn.TermId];
1647
+ termEntry.remaining = rtn.remaining;
1648
+ termEntry.final = rtn.final;
1649
+ const progressDelta = Number(rtn.progressDelta);
1650
+ if (Number.isFinite(progressDelta)) {
1651
+ termEntry.progressCompleted = Math.max(0, (termEntry.progressCompleted ?? 0) + progressDelta);
1652
+ }
1620
1653
  if (rtn.photos.features.length >= 250 && duplicateCount === rtn.photos.features.length) {
1621
1654
  console.error('[ERROR] ALL DUPLICATE');
1622
1655
  }
1623
1656
  const hexCategoryRemaining = Object.values(currentHexCategory.terms).reduce((sum, term) => sum + (term.remaining || 0), 0);
1624
1657
  currentHexCategory.remaining = hexCategoryRemaining;
1625
- currentHexCategory.total = hexCategoryRemaining + idSet.size;
1626
1658
  currentHexCategory.crawled = idSet.size;
1659
+ const reportedExpected = Number(rtn.progressExpected ?? rtn.expected);
1660
+ if (Number.isFinite(reportedExpected) && reportedExpected > 0) {
1661
+ currentHexCategory.progressExpected = Math.max(currentHexCategory.progressExpected ?? 0, reportedExpected);
1662
+ }
1663
+ const fallbackTotal = hexCategoryRemaining + idSet.size;
1664
+ currentHexCategory.total = fallbackTotal;
1665
+ const categoryProgressCompleted = Object.values(currentHexCategory.terms)
1666
+ .reduce((sum, term) => sum + (term.progressCompleted ?? 0), 0);
1667
+ currentHexCategory.progressCompleted = categoryProgressCompleted > 0
1668
+ ? categoryProgressCompleted
1669
+ : undefined;
1670
+ const progressBaseline = Number.isFinite(currentHexCategory.progressCompleted)
1671
+ ? currentHexCategory.progressCompleted
1672
+ : currentHexCategory.crawled;
1673
+ if (currentHexCategory.progressExpected) {
1674
+ currentHexCategory.progressRemaining = Math.max(0, currentHexCategory.progressExpected - progressBaseline);
1675
+ } else {
1676
+ currentHexCategory.progressRemaining = undefined;
1677
+ }
1627
1678
  const allTermsFinal = Object.values(currentHexCategory.terms).every(term => term.final);
1628
1679
  currentHexCategory.final = allTermsFinal && hexCategoryRemaining === 0;
1680
+ if (currentHexCategory.final) {
1681
+ const completedValue = Number.isFinite(currentHexCategory.progressCompleted)
1682
+ ? currentHexCategory.progressCompleted
1683
+ : currentHexCategory.crawled;
1684
+ currentHexCategory.progressCompleted = completedValue;
1685
+ currentHexCategory.progressExpected = completedValue;
1686
+ currentHexCategory.progressRemaining = 0;
1687
+ }
1629
1688
 
1630
1689
  if (argv.debugVerbose) {
1631
1690
  console.log('INFO:', ` ${rtn.hexId} ${rtn.category} ] dup=${duplicateCount}, out=${rtn.outside}, in=${rtn.photos.features.length} || ${currentHexCategory.crawled} / ${currentHexCategory.total}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "splatone",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "description": "Multi-layer Composite Heatmap",
5
5
  "homepage": "https://github.com/YokoyamaLab/Splatone#readme",
6
6
  "bugs": {
@@ -2,9 +2,13 @@
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { mkdir } from 'node:fs/promises';
5
+ import Bottleneck from 'bottleneck';
5
6
  import { ProviderBase } from '../../lib/ProviderBase.js';
6
7
  import { bbox, polygon, centroid, booleanPointInPolygon, featureCollection } from '@turf/turf';
7
8
  import { loadAPIKey } from '#lib/splatone';
9
+
10
+ const DEFAULT_THROTTLE_MAX_CONCURRENT = 2;
11
+ const DEFAULT_THROTTLE_MIN_TIME_MS = 500;
8
12
  export default class FlickrProvider extends ProviderBase {
9
13
 
10
14
  static name = 'Flickr Provider'; // 任意
@@ -13,6 +17,8 @@ export default class FlickrProvider extends ProviderBase {
13
17
  constructor(api, options = {}) {
14
18
  super(api, options);
15
19
  this.id = path.basename(path.dirname(fileURLToPath(import.meta.url)));//必須(ディレクトリ名がプロバイダ名)
20
+ this._throttleLimiter = null;
21
+ this._throttleKey = null;
16
22
  }
17
23
  async yargv(yargv) {
18
24
  // 必須項目にすると、このプラグインを使用しない時も必須になります。
@@ -103,6 +109,16 @@ export default class FlickrProvider extends ProviderBase {
103
109
  return Math.floor(num); // 確実に整数に
104
110
  }
105
111
  throw new Error(`Invalid date/time format: ${opt} (YYYY-MM-DD または UNIX時間(秒)で指定してください)`)
112
+ }).option(this.argKey('ThrottleMaxConcurrent'), {
113
+ group: 'For ' + this.id + ' Provider',
114
+ type: 'number',
115
+ default: DEFAULT_THROTTLE_MAX_CONCURRENT,
116
+ description: 'Flickr API リクエストの同時実行数'
117
+ }).option(this.argKey('ThrottleMinTimeMs'), {
118
+ group: 'For ' + this.id + ' Provider',
119
+ type: 'number',
120
+ default: DEFAULT_THROTTLE_MIN_TIME_MS,
121
+ description: '連続リクエスト間の最小待機時間 (ミリ秒)'
106
122
  });
107
123
  }
108
124
 
@@ -120,9 +136,28 @@ export default class FlickrProvider extends ProviderBase {
120
136
  await mkdir(resolved, { recursive: true });
121
137
  options['GimmeGimme'] = resolved;
122
138
  }
139
+ const throttleMaxConcRaw = Number(options['ThrottleMaxConcurrent'] ?? DEFAULT_THROTTLE_MAX_CONCURRENT);
140
+ const throttleMinTimeRaw = Number(options['ThrottleMinTimeMs'] ?? DEFAULT_THROTTLE_MIN_TIME_MS);
141
+ options['ThrottleMaxConcurrent'] = Number.isFinite(throttleMaxConcRaw) && throttleMaxConcRaw > 0
142
+ ? Math.floor(throttleMaxConcRaw)
143
+ : DEFAULT_THROTTLE_MAX_CONCURRENT;
144
+ options['ThrottleMinTimeMs'] = Number.isFinite(throttleMinTimeRaw) && throttleMinTimeRaw >= 0
145
+ ? Math.floor(throttleMinTimeRaw)
146
+ : DEFAULT_THROTTLE_MIN_TIME_MS;
123
147
  return options;
124
148
  }
125
149
 
150
+ getThrottleLimiter({ ThrottleMaxConcurrent, ThrottleMinTimeMs } = {}) {
151
+ const maxConcurrent = Math.max(1, Math.floor(ThrottleMaxConcurrent ?? DEFAULT_THROTTLE_MAX_CONCURRENT));
152
+ const minTime = Math.max(0, Math.floor(ThrottleMinTimeMs ?? DEFAULT_THROTTLE_MIN_TIME_MS));
153
+ const key = `${maxConcurrent}:${minTime}`;
154
+ if (!this._throttleLimiter || this._throttleKey !== key) {
155
+ this._throttleLimiter = new Bottleneck({ maxConcurrent, minTime });
156
+ this._throttleKey = key;
157
+ }
158
+ return this._throttleLimiter;
159
+ }
160
+
126
161
  async stop() {
127
162
  //this.api.log(`[${this.constructor.id}] stop`);
128
163
  }
@@ -142,6 +177,15 @@ export default class FlickrProvider extends ProviderBase {
142
177
  return featureCollection(selected);
143
178
  }
144
179
  const hexQuery = {};
180
+ const throttleMaxConcurrent = providerOptions?.ThrottleMaxConcurrent ?? this.options?.ThrottleMaxConcurrent ?? DEFAULT_THROTTLE_MAX_CONCURRENT;
181
+ const throttleMinTimeMs = providerOptions?.ThrottleMinTimeMs ?? this.options?.ThrottleMinTimeMs ?? DEFAULT_THROTTLE_MIN_TIME_MS;
182
+ const limiter = this.getThrottleLimiter({ ThrottleMaxConcurrent: throttleMaxConcurrent, ThrottleMinTimeMs: throttleMinTimeMs });
183
+ const resolvedProviderOptions = {
184
+ ...(this.options || {}),
185
+ ...(providerOptions || {}),
186
+ ThrottleMaxConcurrent: throttleMaxConcurrent,
187
+ ThrottleMinTimeMs: throttleMinTimeMs
188
+ };
145
189
  const ks = Object.keys(hexGrid.features);
146
190
  ks.map(k => {
147
191
  const item = hexGrid.features[k];
@@ -151,15 +195,18 @@ export default class FlickrProvider extends ProviderBase {
151
195
  const tags = categories[ck];
152
196
  //console.log("tag=",ck,"/",tags);
153
197
  hexQuery[item.properties.hexId][ck] = { photos: [], tags, final: false };
154
- this.api.emit('splatone:start', { //WorkerOptions
155
- provider: this.id,
156
- hex: item,
157
- triangles: getTrianglesInHex(item, triangles),
158
- bbox: bbox(item.geometry),
159
- category: ck,
160
- tags,
161
- providerOptions,
162
- sessionId
198
+ limiter.schedule(() => {
199
+ this.api.emit('splatone:start', { //WorkerOptions
200
+ provider: this.id,
201
+ hex: item,
202
+ triangles: getTrianglesInHex(item, triangles),
203
+ bbox: bbox(item.geometry),
204
+ category: ck,
205
+ tags,
206
+ providerOptions: resolvedProviderOptions,
207
+ sessionId
208
+ });
209
+ return null;
163
210
  });
164
211
  });
165
212
  });