splatone 0.0.13 → 0.0.14

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/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## Versions
4
4
 
5
+ ### v0.0.11 → v0.0.12
6
+
7
+ * Bottleneckを導入しクエリ間隔を適正値に調整 (3 queries/ 3 sec.)
8
+ * 時間軸分割並列処理のデフォルト化
9
+ * 地理的分割に加えて大量の結果がある場所は時間軸でもクエリを分解する
10
+ * 無効にするときは```--no-p-flickr-Haste```を付与
11
+
12
+ ### v0.0.10 → v0.0.11
13
+
14
+ * 時間軸として使用する日付を選択可能に (```--p-flickr-DateMode```)
15
+ * upload: Flickrにアップロードされたタイムスタンプを遡ってクローリング (デフォルト)
16
+ * taken: 写真の撮影日時を遡ってクローリング
17
+ * extrasを指定可能に (```--p-flickr-Extras```)
18
+ * https://www.flickr.com/services/api/explore/flickr.photos.search
19
+ * デフォルト値: ```date_upload,date_taken,owner_name,geo,url_s,tags```
20
+ * これらはコマンドライン引数での指定の有無に関わらず付与されます
21
+ * 自動指定時のHexGridの最小サイズを0.5kmに
22
+ * [Bug Fix] 時間軸並列機能のバグ修正
23
+
24
+ ### v0.0.8 → v0.0.9 → v0.0.10
25
+
26
+ * 【重要】**APIキー**の指定方法が変わりました。
27
+ * ```--p-flickr-APIKEY```オプションを使います。
28
+ * クエリを時間方向でも分割し効率化しました。(使い方に変更はありません)
29
+
30
+
5
31
  ### v0.0.7 → v0.0.8
6
32
 
7
33
  * 範囲指定とHexGridの表示・非表示ができるようになりました。
package/README.md CHANGED
@@ -13,35 +13,14 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
13
13
 
14
14
  ## Change Log
15
15
 
16
+ ### v0.0.13 → v0.0.14
17
+ * **[可視化モジュール]** ```--vis-majority-hex```追加
18
+ * 結果の色固定機能追加 (キーワード指定方法を参照の事)
19
+
16
20
  ### v0.0.12 → v0.0.13
17
21
  * BulkyのPointMarkerのサイズや透明度を可変に
18
22
  * コマンドライン引数で指定 (詳しくは``` npx -y -- splatone@latest crawler --help```)
19
23
 
20
- ### v0.0.11 → v0.0.12
21
-
22
- * Bottleneckを導入しクエリ間隔を適正値に調整 (3 queries/ 3 sec.)
23
- * 時間軸分割並列処理のデフォルト化
24
- * 地理的分割に加えて大量の結果がある場所は時間軸でもクエリを分解する
25
- * 無効にするときは```--no-p-flickr-Haste```を付与
26
-
27
- ### v0.0.10 → v0.0.11
28
-
29
- * 時間軸として使用する日付を選択可能に (```--p-flickr-DateMode```)
30
- * upload: Flickrにアップロードされたタイムスタンプを遡ってクローリング (デフォルト)
31
- * taken: 写真の撮影日時を遡ってクローリング
32
- * extrasを指定可能に (```--p-flickr-Extras```)
33
- * https://www.flickr.com/services/api/explore/flickr.photos.search
34
- * デフォルト値: ```date_upload,date_taken,owner_name,geo,url_s,tags```
35
- * これらはコマンドライン引数での指定の有無に関わらず付与されます
36
- * 自動指定時のHexGridの最小サイズを0.5kmに
37
- * [Bug Fix] 時間軸並列機能のバグ修正
38
-
39
- ### v0.0.8 → v0.0.9 → v0.0.10
40
-
41
- * 【重要】**APIキー**の指定方法が変わりました。
42
- * ```--p-flickr-APIKEY```オプションを使います。
43
- * クエリを時間方向でも分割し効率化しました。(使い方に変更はありません)
44
-
45
24
  [これ以前のログ](CHANGELOG.md)
46
25
 
47
26
 
@@ -59,7 +38,6 @@ $ npx -y -- splatone@latest crawler --help
59
38
 
60
39
  Basic Options
61
40
  -p, --plugin 実行するプラグイン[文字列] [必須] [選択してください: "flickr"]
62
- -o, --options プラグインオプション [文字列] [デフォルト: "{}"]
63
41
  -k, --keywords 検索キーワード(|区切り) [文字列] [デフォルト:
64
42
  "nature,tree,flower|building,house|water,sea,river,pond"]
65
43
  -f, --filed 大きなデータをファイルとして送受信する
@@ -74,57 +52,150 @@ For flickr Plugin
74
52
  --p-flickr-APIKEY Flickr ServiceのAPI KEY [文字列]
75
53
  --p-flickr-Extras カンマ区切り/保持する写真のメタデータ(デフォルト値は
76
54
  記載の有無に関わらず保持)
77
- [文字列] [デフォルト: "date_upload,date_taken,owner_name,geo,url_s,tags"]
78
- --p-flickr-DateMode 利用時間軸(update=Flickr投稿日時/taken=写真撮影日時)
79
- [選択してください: "upload", "taken"] [デフォルト: "upload"]
80
- --p-flickr-Haste 時間軸分割並列処理 [真偽] [デフォルト: true]
81
- --p-flickr-DateMax クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD
82
- [文字列] [デフォルト: 1762942310]
83
- --p-flickr-DateMin クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD
84
- [文字列] [デフォルト: 1072882800]
55
+ [文字列] [デフォルト: "date_upload,date_taken,owner_name,geo,url_s,tags"]
56
+ --p-flickr-DateMode 利用時間軸(update=Flickr投稿日時/taken=写真撮影日時)
57
+ [選択してください: "upload", "taken"] [デフォルト: "upload"]
58
+ --p-flickr-Haste 時間軸分割並列処理 [真偽] [デフォルト: true]
59
+ --p-flickr-DateMax クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD
60
+ [文字列] [デフォルト: 1763107393]
61
+ --p-flickr-DateMin クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD
62
+ [文字列] [デフォルト: 1072882800]
85
63
 
86
64
  Visualization (最低一つの指定が必須です)
87
65
  --vis-bulky 全データをCircleMarkerとして地図上に表示
88
- [真偽] [デフォルト: false]
66
+ [真偽] [デフォルト: false]
67
+ --vis-majority-hex HexGrid内で最も出現頻度が高いカテゴリの色で彩色。Hex
68
+ apartiteモードで6分割パイチャート表示。透明度は全体
69
+ で正規化。 [真偽] [デフォルト: false]
89
70
  --vis-marker-cluster マーカークラスターとして地図上に表示
90
- [真偽] [デフォルト: false]
71
+ [真偽] [デフォルト: false]
91
72
 
92
73
  For bulky Visualizer
93
- --v-bulky-Radius Point Markerの直径 [数値] [デフォルト: 5]
94
- --v-bulky-Stroke Point Markerの線の有無 [真偽] [デフォルト: true]
95
- --v-bulky-Weight Point Markerの線の太さ [数値] [デフォルト: 1]
96
- --v-bulky-Opacity Point Markerの線の透明度 [数値] [デフォルト: 1]
97
- --v-bulky-Filling Point Markerの塗りの有無 [真偽] [デフォルト: true]
98
- --v-bulky-FillOpacity Point Markerの塗りの透明度 [数値] [デフォルト: 0.5]
74
+ --v-bulky-Radius Point Markerの半径 [数値] [デフォルト: 5]
75
+ --v-bulky-Stroke Point Markerの線の有無 [真偽] [デフォルト: true]
76
+ --v-bulky-Weight Point Markerの線の太さ [数値] [デフォルト: 1]
77
+ --v-bulky-Opacity Point Markerの線の透明度 [数値] [デフォルト: 1]
78
+ --v-bulky-Filling Point Markerの塗りの有無 [真偽] [デフォルト: true]
79
+ --v-bulky-FillOpacity Point Markerの塗りの透明度 [数値] [デフォルト: 0.5]
80
+
81
+ For majority-hex Visualizer
82
+ --v-majority-hex-Hexapartite 中のカテゴリの頻度に応じて六角形を分割色彩
83
+ [真偽] [デフォルト: false]
84
+ --v-majority-hex-HexOpacity 六角形の線の透明度 [数値] [デフォルト: 1]
85
+ --v-majority-hex-HexWeight 六角形の線の太さ [数値] [デフォルト: 1]
86
+ --v-majority-hex-MaxOpacity 正規化後の最大塗り透明度
87
+ [数値] [デフォルト: 0.9]
88
+ --v-majority-hex-MinOpacity 正規化後の最小塗り透明度
89
+ [数値] [デフォルト: 0.5]
90
+
91
+ For marker-cluster Visualizer
92
+ --v-marker-cluster-MaxClusterRadius クラスタを構成する範囲(半径)
93
+ [数値] [デフォルト: 80]
99
94
 
100
95
  オプション:
101
- --help ヘルプを表示 [真偽]
102
- --version バージョンを表示 [真偽]
96
+ --help ヘルプを表示 [真偽]
97
+ --version バージョンを表示 [真偽]
103
98
 
104
- ```
105
- ## クローリングの実行
99
+ cold_@bimota-due MINGW64 /c/GitHub/Splatone (61-可視化メソッドmajorityhex)
100
+ $ node crawler.js --help
101
+ [app] [plugin] loaded: flickr@1.0.0
102
+ 使い方: crawler.js [options]
103
+
104
+ Basic Options
105
+ -p, --plugin 実行するプラグイン[文字列] [必須] [選択してください: "flickr"]
106
+ -k, --keywords 検索キーワード(|区切り) [文字列] [デフォルト:
107
+ "nature,tree,flower|building,house|water,sea,river,pond"]
108
+ -f, --filed 大きなデータをファイルとして送受信する
109
+ [真偽] [デフォルト: true]
110
+ -c, --chopped 大きなデータを細分化して送受信する
111
+ [非推奨] [真偽] [デフォルト: false]
106
112
 
107
- - 以下のサンプルコマンドを参考に実行してください。
108
- - **FlickrのAPIキーは自身のに置き換える事**
109
- - ブラウザが立ち上がるので地図上でポリゴンあるいは矩形で領域選択し、実行ボタンを押すとクロールが開始されます。
110
- - 指定した範囲を内包するHexGrid(六角形グリッド)が生成され、その内側のみが収集されます。
111
- - 結果が表示された後、結果をGeoJSON形式でダウンロードできます。
113
+ Debug
114
+ --debug-verbose デバッグ情報出力 [真偽] [デフォルト: false]
112
115
 
113
- ### 事例1) 商業施設・飲食施設・文化施設・公園の分類
114
- ```shell
115
- $ node crawler.js -p flickr -k "商業=shop,souvenir,market,supermarket,pharmacy,drugstore,store,department,kiosk,bazaar,bookstore,cinema,showroom|飲食=bakery,food,drink,restaurant,cafe,bar,beer,wine,whiskey|文化施設=museum,gallery,theater,concert,library,monument,exhibition,expo,sculpture,heritage|公園=park,garden,flower,green,pond,playground" --vis-bulky --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
116
+ For flickr Plugin
117
+ --p-flickr-APIKEY Flickr ServiceのAPI KEY [文字列]
118
+ --p-flickr-Extras カンマ区切り/保持する写真のメタデータ(デフォルト値は
119
+ 記載の有無に関わらず保持)
120
+ [文字列] [デフォルト: "date_upload,date_taken,owner_name,geo,url_s,tags"]
121
+ --p-flickr-DateMode 利用時間軸(update=Flickr投稿日時/taken=写真撮影日時)
122
+ [選択してください: "upload", "taken"] [デフォルト: "upload"]
123
+ --p-flickr-Haste 時間軸分割並列処理 [真偽] [デフォルト: true]
124
+ --p-flickr-DateMax クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD
125
+ [文字列] [デフォルト: 1763107399]
126
+ --p-flickr-DateMin クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD
127
+ [文字列] [デフォルト: 1072882800]
128
+
129
+ Visualization (最低一つの指定が必須です)
130
+ --vis-bulky 全データをCircleMarkerとして地図上に表示
131
+ [真偽] [デフォルト: false]
132
+ --vis-majority-hex HexGrid内で最も出現頻度が高いカテゴリの色で彩色。Hex
133
+ apartiteモードで6分割パイチャート表示。透明度は全体
134
+ で正規化。 [真偽] [デフォルト: false]
135
+ --vis-marker-cluster マーカークラスターとして地図上に表示
136
+ [真偽] [デフォルト: false]
137
+
138
+ For bulky Visualizer
139
+ --v-bulky-Radius Point Markerの半径 [数値] [デフォルト: 5]
140
+ --v-bulky-Stroke Point Markerの線の有無 [真偽] [デフォルト: true]
141
+ --v-bulky-Weight Point Markerの線の太さ [数値] [デフォルト: 1]
142
+ --v-bulky-Opacity Point Markerの線の透明度 [数値] [デフォルト: 1]
143
+ --v-bulky-Filling Point Markerの塗りの有無 [真偽] [デフォルト: true]
144
+ --v-bulky-FillOpacity Point Markerの塗りの透明度 [数値] [デフォルト: 0.5]
145
+
146
+ For majority-hex Visualizer
147
+ --v-majority-hex-Hexapartite 中のカテゴリの頻度に応じて六角形を分割色彩
148
+ [真偽] [デフォルト: false]
149
+ --v-majority-hex-HexOpacity 六角形の線の透明度 [数値] [デフォルト: 1]
150
+ --v-majority-hex-HexWeight 六角形の線の太さ [数値] [デフォルト: 1]
151
+ --v-majority-hex-MaxOpacity 正規化後の最大塗り透明度
152
+ [数値] [デフォルト: 0.9]
153
+ --v-majority-hex-MinOpacity 正規化後の最小塗り透明度
154
+ [数値] [デフォルト: 0.5]
155
+
156
+ For marker-cluster Visualizer
157
+ --v-marker-cluster-MaxClusterRadius クラスタを構成する範囲(半径)
158
+ [数値] [デフォルト: 80]
159
+
160
+ オプション:
161
+ --help ヘルプを表示 [真偽]
162
+ --version バージョンを表示 [真偽]
116
163
  ```
117
- - オプションの **--vis-bulky** を **--vis-marker-cluster** に変更する事でマーカークラスターで可視化できます。
118
164
 
119
- ### 事例2)水路・陸路・ランドマーク等の分類
120
- ```shell
121
- $ 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 --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
165
+ ## 最小コマンド例
166
+
167
+ 1. *plugin*を一つ、*visualizer*を一つ以上指定し、複数のキーワードでクロールを開始します。
168
+ * plugin: flickr
169
+ * visualizer: bulky
170
+ * キーワード: canal,river|street,alley|bridge
171
+ 1. コマンドを実行するとWebブラウザで地図表示されるので、地図上の任意の位置に矩形あるいはポリゴンを描く
172
+ * 例えばベネチア
173
+ 2. Start Crawlingボタンをクリックしクローリング開始
174
+
175
+ ![](assets/screenshot_venice_simple.png?raw=true)
176
+
177
+ ```bash
178
+ $ npx -y -- splatone@latest crawler -p flickr -k "canal,river|street,alley|bridge" --vis-bulky --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
122
179
  ```
123
- - ベネチア等の水路のある町でやると面白いです
124
180
 
125
181
  # 詳細説明
126
182
 
127
- ## Flickr APIキーの与え方
183
+ ## Plugin (クローラー)
184
+
185
+ ### Flickr: Flickrのジオタグ付き写真を取得するクローラー
186
+
187
+ #### コマンドライン引数
188
+
189
+ | オプション | 説明 | 型 | デフォルト |
190
+ | :------------------------ | :---------------------------------------------------------------------------- | :------------- | :----------- |
191
+ | ```--p-flickr-APIKEY``` | Flickr ServiceのAPI KEY | 文字列 | |
192
+ | ```--p-flickr-Extras``` | カンマ区切り/保持する写真のメタデータ(デフォルト値は記載の有無に関わらず保持) | 文字列 | date_upload |,date_taken,owner_name,geo,url_s,tags
193
+ | ```--p-flickr-DateMode``` | 利用時間軸(update=Flickr投稿日時/taken=写真撮影日時) | 選択: "upload" | "taken" |,"upload"
194
+ | ```--p-flickr-Haste``` | 時間軸分割並列処理 | 真偽 | true |
195
+ | ```--p-flickr-DateMax``` | クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD | 文字列 | (動的)現時刻 |
196
+ | ```--p-flickr-DateMin``` | クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD | 文字列 | 1072882800 |
197
+
198
+ #### Flickr APIキーの与え方
128
199
 
129
200
  APIキーは以下の3種類の方法で与える事ができます
130
201
  - ```--option```に含める
@@ -134,34 +205,74 @@ APIキーは以下の3種類の方法で与える事ができます
134
205
  - 環境変数で渡す
135
206
  - ```API_KEY_plugin```という環境変数に格納する
136
207
  - コマンドに毎回含めなくて良くなる。
137
- - **flickr**の場合は```API_KEY_flickr```になります。
138
- - ```plugin```はプラグイン名(flickr等)に置き換えてください。
139
- - 一時的な環境変数を定義する事も可能です。(bash等)
140
- - ```API_KEY_flickr="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" node crawler.js -p flickr -k "sea,ocean|mountain,mount" --vis-bulky```
141
208
  - ファイルで渡す(npxでは不可)
142
209
  - ルートディレクトリに```.API_KEY.plugin```というファイルを作成し保存
143
210
  - ```plugin```はプラグイン名(flickr等)に置き換えてください。
144
211
  - **flickr**の場合は```.API_KEY.flickr```になります。
145
212
  - optionや環境変数で与えるよりも優先されます。
146
213
 
147
- ## Visualizer (可視化ツール)
214
+ ## Visualizer (可視化モジュール)
148
215
 
149
216
  ### Bulky: 全ての点を地図上にポイントする
150
217
 
151
- ![](https://github.com/YokoyamaLab/Splatone/blob/main/assets/screenshot_venice_bulky.png?raw=true)
218
+ ![](assets/screenshot_sea-mountain_bulky.png?raw=true)
152
219
 
153
- * クエリは水域と通路・橋梁・ランドマークを色分けしたもの、上記スクリーンショットはベネチア付近のデータ
220
+ #### コマンド例
221
+ * クエリは海と山のキーワード検索。上記スクリーンショットは日本のデータ
154
222
  ```shell
155
- $ 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" --vis-marker-cluster --vis-bulky --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
223
+ $ npx -y -- splatone@latest crawler -p flickr -k "sea,ocean|mountain,mount" --vis-bulky--p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
156
224
  ```
157
225
 
226
+ #### コマンドライン引数
227
+
228
+ | オプション | 説明 | 型 | デフォルト |
229
+ | :-------------------------- | :------------------------- | :--- | :--------- |
230
+ | ```--v-bulky-Radius``` | Point Markerの半径 | 数値 | 5 |
231
+ | ```--v-bulky-Stroke``` | Point Markerの線の有無 | 真偽 | true |
232
+ | ```--v-bulky-Weight``` | Point Markerの線の太さ | 数値 | 1 |
233
+ | ```--v-bulky-Opacity``` | Point Markerの線の透明度 | 数値 | 1 |
234
+ | ```--v-bulky-Filling``` | Point Markerの塗りの有無 | 真偽 | true |
235
+ | ```--v-bulky-FillOpacity``` | Point Markerの塗りの透明度 | 数値 | 0.5 |
236
+
237
+
158
238
  ### Marker Cluster: 高密度の地点はマーカーをまとめて表示する
159
- ![](https://github.com/YokoyamaLab/Splatone/blob/main/assets/screenshot_venice_marker-cluster.png?raw=true)
239
+ ![](assets/screenshot_venice_marker-cluster.png?raw=true)
160
240
 
241
+ #### コマンド例
161
242
  * クエリは水域と通路・橋梁・ランドマークを色分けしたもの、上記スクリーンショットはベネチア付近のデータ
162
243
  ```shell
163
- $ 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" --vis-marker-cluster --vis-bulky --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
244
+ $ npx -y -- splatone@latest crawler -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" --vis-marker-cluster --vis-bulky --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
245
+ ```
246
+
247
+ #### コマンドライン引数
248
+
249
+ | オプション | 説明 | 型 | デフォルト |
250
+ | :---------------------------------------- | :--------------------------- | :--- | :--------- |
251
+ | ```--v-marker-cluster-MaxClusterRadius``` | クラスタを構成する範囲(半径) | 数値 | 80 |
252
+
253
+ ### Majority Hex: Hexグリッド内の出現頻度に応じた彩色
254
+
255
+ ![](assets/screenshot_florida_hex_majorityr.png?raw=true)
256
+
257
+ #### コマンド例
258
+ * クエリは水域・緑地・交通・ランドマークを色分けしたもの。上記スクリーンショットはフロリダ半島全体
259
+ *
260
+ ```shell
261
+ $ npx -y -- 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"
164
262
  ```
263
+
264
+ #### コマンドライン引数
265
+
266
+ | オプション | 説明 | 型 | デフォルト |
267
+ | :------------------------------------ | :----------------------------------------- | :--- | :--------- |
268
+ | ```--v-majority-hex-Hexapartite``` | 中のカテゴリの頻度に応じて六角形を分割色彩 | 真偽 | false |
269
+ | ```--v-majority-hex-HexOpacity=1``` | 六角形の線の透明度 | 数値 | 1 |
270
+ | ```--v-majority-hex-HexWeight=1``` | 六角形の線の太さ | 数値 | 1 |
271
+ | ```--v-majority-hex-MaxOpacity=0.9``` | 正規化後の最大塗り透明度 | 数値 | 0.9 |
272
+ | ```--v-majority-hex-MinOpacity=0.3``` | 正規化後の最小塗り透明度 | 数値 | 0.5 |
273
+
274
+ * ```--v-majority-hex-Hexapartite```を指定すると各Hexセルを六分割の荒いPie Chartとして中のカテゴリ頻度に応じて彩色します。
275
+
165
276
  ## キーワード指定方法
166
277
 
167
278
  ### 比較キーワードの指定
@@ -188,30 +299,59 @@ seaだけでは集められるポストが限定されるので、同様の意
188
299
  -k "海域=sea,ocean|山岳=mountain,mount"
189
300
  ```
190
301
 
191
- ### 実行例 (海岸線と山岳の分布)
302
+ ### カテゴリ毎の色指定
303
+
304
+ カテゴリの内容に合わせた色を指定したい場合はコマンドライン引数にて行えます。例えば海域を青に、山岳を緑にしたい場合は、カテゴリ名に続けて**#RRGGBB**で指定します。
192
305
 
193
- ```shell
194
- $ node crawler.js -p flickr -k "sea,ocean|mountain,mount" --vis-bulky--p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
195
306
  ```
196
- ![](https://github.com/YokoyamaLab/Splatone/blob/main/assets/screenshot_sea-mountain_bulky.png?raw=true)
307
+ -k "海域#037dfc=sea,ocean|山岳#7fc266=mountain,mount"
308
+ ```
309
+
310
+ 色を簡単に探すための小さなコマンドが付属しています。
311
+
312
+ #### 色セット生成ツール(color.js)の使い方
313
+
314
+ このリポジトリには、コマンドラインで色のセットを生成する小さなユーティリティ `color.js` が含まれています。用途は以下の通りです。
197
315
 
316
+ - 指定した数のカラーパレット(セット)を生成する
317
+ - ターミナル上で色サンプルを ANSI Truecolor で確認する
318
+ - プレーンなカンマ区切り HEX リストを出力して他ツールに渡す
319
+
320
+ - 使い方(6色のカラーパレットを2セット作りたい):
321
+
322
+ ```bash
323
+ npx -y -- splatone@latest color <count> <sets>
324
+ # 例: 6色を3セット生成(ターミナルに色付きで表示)
325
+ npx -y -- splatone@latest color 6 3
326
+ ```
327
+
328
+ - オプション:
329
+
330
+ - `--no-ansi` : ANSI カラーシーケンスを出力せず、プレーンなカンマ区切りの HEX を出力します(パイプやログ向け)。
331
+
332
+ ```bash
333
+ npx -y -- splatone@latest color --no-ansi 6 3
334
+ ```
198
335
 
199
336
  ## ダウンロード
200
337
 
201
338
  ### 画像のダウンロード
202
339
 
203
340
  * 結果の地図を画像(PNG形式)としてダウンロードするには、画面右下のアイコンをクリックしてください。
341
+ * 注意: 画像には凡例が含まれません
204
342
 
205
- ![](https://github.com/YokoyamaLab/Splatone/blob/main/assets/icon_image_download.png?raw=true)
343
+ ![](assets/icon_image_download.png?raw=true)
206
344
 
207
345
  ### データのダウンロード
208
346
 
209
347
  * クロール結果をデータとしてダウンロードしたい場合は凡例の下にあるエクスポートボタンをクリックしてください。
348
+ * 指定したビジュアライザ毎にFeature Collectionとして結果が格納されます。
349
+ * クローリングしたデータそのものが欲しい場合はBulky等、単純なビジュアライザを指定してください。
210
350
 
211
- ![](https://github.com/YokoyamaLab/Splatone/blob/main/assets/icon_data_export.png?raw=true)
351
+ ![](assets/icon_data_export.png?raw=true)
212
352
 
213
353
  ### 広範囲なデータ収集例
214
354
 
215
- * あまりにも大きいとFlickrから一時的にBANされることがありますので注意してください。
355
+ * クエリ数はおおよそ1 query/secに調整されますので、時間はかかりますが大量のデータを収集する事も可能です。
216
356
 
217
357
  ![](/assets/screenshot_massive_points_bulky.png)
package/color.js ADDED
@@ -0,0 +1,95 @@
1
+ import paletteGenerator from './lib/paletteGenerator.js';
2
+
3
+ // Usage: node color.js <count> <sets>
4
+ // Example: node color.js 6 3
5
+ // Outputs HTML lines, each line is one set: comma-separated spans where
6
+ // each span shows the hex string in that color (text color set to the color).
7
+
8
+ function usage() {
9
+ console.log('Usage: node color.js <count> <sets>');
10
+ console.log(' count: number of colors per set (default 8)');
11
+ console.log(' sets: number of sets to generate (default 1)');
12
+ console.log(' --no-ansi: disable ANSI color escapes and output plain comma-separated HEX values');
13
+ }
14
+
15
+ async function main() {
16
+ let argv = process.argv.slice(2) || [];
17
+ if (argv.length === 0) {
18
+ usage();
19
+ return;
20
+ }
21
+
22
+ const noAnsi = argv.indexOf('--no-ansi') !== -1;
23
+ argv = argv.filter(a => a !== '--no-ansi');
24
+
25
+ const count = parseInt(argv[0], 10) || 8;
26
+ const sets = parseInt(argv[1], 10) || 1;
27
+
28
+ for (let s = 0; s < sets; s++) {
29
+ // Try to generate a palette via paletteGenerator; if it fails, fallback to HSL per-set
30
+ let cols = null;
31
+ try {
32
+ if (typeof Math.random.seed === 'function') {
33
+ Math.random.seed(Date.now() + s);
34
+ }
35
+ cols = paletteGenerator.generate(
36
+ count,
37
+ function (color) {
38
+ var hcl = color.hcl();
39
+ return hcl[0] >= 0 && hcl[0] <= 360
40
+ && hcl[1] >= 54.96 && hcl[1] <= 134
41
+ && hcl[2] >= 19.14 && hcl[2] <= 90.23;
42
+ },
43
+ true,
44
+ 50,
45
+ false,
46
+ 'CMC'
47
+ );
48
+ } catch (e) {
49
+ cols = null;
50
+ }
51
+
52
+ function hslToHex(h, s, l) {
53
+ s = Math.max(0, Math.min(1, s));
54
+ l = Math.max(0, Math.min(1, l));
55
+ const a = s * Math.min(l, 1 - l);
56
+ function f(n) {
57
+ const k = (n + h / 30) % 12;
58
+ const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
59
+ return Math.round(255 * color).toString(16).padStart(2, '0');
60
+ }
61
+ return `#${f(0)}${f(8)}${f(4)}`;
62
+ }
63
+
64
+ const hexes = (cols ? cols.map(c => (typeof c.hex === 'function' ? c.hex() : String(c))) :
65
+ Array.from({ length: count }, (_, i) => {
66
+ const hue = Math.round(((i / count) * 360 + s * 37) % 360);
67
+ return hslToHex(hue, 0.65, 0.5);
68
+ })
69
+ );
70
+
71
+ if (noAnsi) {
72
+ console.log(hexes.join(','));
73
+ } else {
74
+ const esc = (r, g, b) => `\u001b[38;2;${r};${g};${b}m`;
75
+ const reset = '\u001b[0m';
76
+ function hexToRgb(hex) {
77
+ const h = hex.replace('#', '');
78
+ const bigint = parseInt(h, 16);
79
+ return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
80
+ }
81
+ const out = hexes.map(h => {
82
+ const rgb = hexToRgb(h);
83
+ return `${esc(...rgb)}${h}${reset}`;
84
+ }).join(',');
85
+ console.log(out);
86
+ }
87
+ }
88
+ }
89
+
90
+ main().catch(err => {
91
+ console.error(err);
92
+ process.exit(1);
93
+ });
94
+
95
+
package/crawler.js CHANGED
@@ -30,6 +30,7 @@ import { hideBin } from 'yargs/helpers';
30
30
  // -------------------------------
31
31
  import { loadPlugins } from './lib/pluginLoader.js';
32
32
  import paletteGenerator from './lib/paletteGenerator.js';
33
+ import chroma from 'chroma-js';
33
34
  import { dfsObject, bboxSize, saveGeoJsonObjectAsStream, buildPluginsOptions, loadAPIKey, buildVisualizersOptions } from '#lib/splatone';
34
35
 
35
36
  const __filename = fileURLToPath(import.meta.url);
@@ -116,12 +117,13 @@ try {
116
117
  express.static(resolve(VIZ_BASE, name, 'public'))(req, res, next);
117
118
  });
118
119
  // コマンド例
119
- // 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
120
- // 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
120
+ // 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
121
121
 
122
- // node crawler.js -p flickr -k "水辺=sea,ocean,beach,river,delta,lake,coast,creek|緑地=forest,woods,turf,lawn,jungle,trees,rainforest,grove,savanna,steppe|砂漠=desert,dune,outback,barren,wasteland" --vis-bulky --filed --p-flickr-Haste --p-flickr-DateMode=taken
122
+ // node crawler.js -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-bulky
123
123
 
124
- // node crawler.js -p flickr -k "商業=shop,souvenir,market,supermarket,pharmacy,drugstore,store,department,kiosk,bazaar,bookstore,cinema,showroom|飲食=bakery,food,drink,restaurant,cafe,bar,beer,wine,whiskey|文化施設=museum,gallery,theater,concert,library,monument,exhibition,expo,sculpture,heritage|公園=park,garden,flower,green,pond,playground" --vis-bulky --p-flickr-Hates --p-flickr-DateMode=taken
124
+ // node crawler.js -p flickr -k "水辺=sea,ocean,beach,river,delta,lake,coast,creek|緑地=forest,woods,turf,lawn,jungle,trees,rainforest,grove,savanna,steppe|砂漠=desert,dune,outback,barren,wasteland" --vis-bulky
125
+
126
+ // node crawler.js -p flickr -k "商業=shop,souvenir,market,supermarket,pharmacy,drugstore,store,department,kiosk,bazaar,bookstore,cinema,showroom|飲食=bakery,food,drink,restaurant,cafe,bar,beer,wine,whiskey|文化施設=museum,gallery,theater,concert,library,monument,exhibition,expo,sculpture,heritage|公園=park,garden,flower,green,pond,playground" --vis-bulky
125
127
 
126
128
  let yargv = await yargs(hideBin(process.argv))
127
129
  .strict() // 未定義オプションはエラー
@@ -134,13 +136,6 @@ try {
134
136
  describe: '実行するプラグイン',
135
137
  type: 'string'
136
138
  })
137
- .option('options', {
138
- group: 'Basic Options',
139
- alias: 'o',
140
- default: '{}',
141
- describe: 'プラグインオプション',
142
- type: 'string'
143
- })
144
139
  .option('keywords', {
145
140
  group: 'Basic Options',
146
141
  alias: 'k',
@@ -173,10 +168,12 @@ try {
173
168
  })
174
169
  .version()
175
170
  .coerce({
171
+ /*
176
172
  options: ((name) => (v) => {
177
173
  try { return JSON.parse(v); }
178
174
  catch (e) { throw new Error(`--${name}: JSON エラー: ${e.message}`); }
179
175
  })()
176
+ */
180
177
  });
181
178
  plugins.list().forEach(async (plug) => {
182
179
  yargv = await plugins.call(plug, "yargv", yargv);
@@ -384,12 +381,27 @@ try {
384
381
  }
385
382
 
386
383
  //カテゴリ生成
384
+ // キーにカラー指定が含まれている場合 (例: "水域#ff0000=canal,river") は
385
+ // カラー部分をキー名から取り除き、表示用の純粋なラベルをキーとして返す。
386
+ // 明示色があれば explicitColors に記録しておき、後でパレット生成時に利用する。
387
+ const explicitColors = {};
387
388
  const categorize = (tags) => {
388
389
  let cats = {};
389
390
  tags.split('|').forEach((tag_set, i) => {
390
391
  const key_val = tag_set.split("=", 2);
391
- const key = (key_val.length == 1) ? key_val[0].split(",")[0] : key_val[0];
392
+ // key_raw は元のキー(色コードを含む可能性あり)
393
+ const key_raw = (key_val.length == 1) ? key_val[0].split(",")[0] : key_val[0];
392
394
  const val = (key_val.length == 1) ? key_val[0] : key_val[1];
395
+ // カラー指定を抽出してキー名から除去
396
+ const hexMatch = String(key_raw).match(/#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/);
397
+ let key = key_raw;
398
+ if (hexMatch) {
399
+ const explicit = hexMatch[0];
400
+ // display label から色コードを除去してトリム
401
+ key = key_raw.replace(explicit, '').trim();
402
+ // store explicit color for this cleaned key
403
+ explicitColors[key] = explicit;
404
+ }
393
405
  cats[key] = val;
394
406
  });
395
407
  return cats;
@@ -413,12 +425,22 @@ try {
413
425
  // Sort colors by differenciation first
414
426
  const palette = paletteGenerator.diffSort(colors, 'Default');
415
427
  const splatonePalette = Object.fromEntries(Object.entries(categories).map(([k, v]) => {
416
- const color = palette.pop()
417
- const colors = {
418
- "color": color.hex(),
419
- "darken": color.darken(2).hex(),
420
- "brighten": color.brighten(2).hex()
428
+ // explicitColors にあればその色を使う(キーは既に色コードを取り除いた表示名)
429
+ if (explicitColors.hasOwnProperty(k)) {
430
+ const explicit = explicitColors[k];
431
+ const colors = {
432
+ color: chroma(explicit).hex(),
433
+ darken: chroma(explicit).darken(2).hex(),
434
+ brighten: chroma(explicit).brighten(2).hex()
435
+ };
436
+ return [k, colors];
421
437
  }
438
+ const color = palette.pop();
439
+ const colors = {
440
+ color: color.hex(),
441
+ darken: color.darken(2).hex(),
442
+ brighten: color.brighten(2).hex()
443
+ };
422
444
  return [k, colors];
423
445
  }));
424
446
 
@@ -42,7 +42,6 @@ Math.random.seed = (function me (s) {
42
42
  Math.random.seed = me;
43
43
  return me;
44
44
  })(0);
45
- Math.random.seed(25);
46
45
 
47
46
  const paletteGenerator = (function(undefined){
48
47
  const ns = {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "splatone",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "Multi-layer Composite Heatmap",
5
5
  "homepage": "https://github.com/YokoyamaLab/Splatone#readme",
6
6
  "bugs": {
@@ -15,7 +15,8 @@
15
15
  "type": "module",
16
16
  "main": "index.js",
17
17
  "bin": {
18
- "crawler": "./crawler.js"
18
+ "crawler": "./crawler.js",
19
+ "color": "./color.js"
19
20
  },
20
21
  "imports": {
21
22
  "#lib/*": "./lib/*.js"
@@ -18,7 +18,7 @@ export default class BulkyVisualizer extends VisualizerBase {
18
18
  return yargv.option(this.argKey('Radius'), {
19
19
  group: 'For ' + this.id + ' Visualizer',
20
20
  type: 'number',
21
- description: 'Point Markerの直径',
21
+ description: 'Point Markerの半径',
22
22
  default: 5
23
23
  }).option(this.argKey('Stroke'), {
24
24
  group: 'For ' + this.id + ' Visualizer',
@@ -0,0 +1,214 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { VisualizerBase } from '../../lib/VisualizerBase.js';
4
+ import { featureCollection } from "@turf/turf";
5
+
6
+ export default class MajorityHex extends VisualizerBase {
7
+ static name = 'MajorityHex Visualizer';
8
+ static version = '0.0.2';
9
+ static description = "HexGrid内で最も出現頻度が高いカテゴリの色で彩色。Hexapartiteモードで6分割パイチャート表示。透明度は全体で正規化。";
10
+
11
+ constructor() {
12
+ super();
13
+ this.id = path.basename(path.dirname(fileURLToPath(import.meta.url)));
14
+ }
15
+
16
+ async yargv(yargv) {
17
+ return yargv.option(this.argKey('Hexapartite'), {
18
+ group: 'For ' + this.id + ' Visualizer',
19
+ type: 'boolean',
20
+ description: '中のカテゴリの頻度に応じて六角形を分割色彩',
21
+ default: false
22
+ }).option(this.argKey('HexOpacity'), {
23
+ group: 'For ' + this.id + ' Visualizer',
24
+ type: 'number',
25
+ description: '六角形の線の透明度',
26
+ default: 1
27
+ }).option(this.argKey('HexWeight'), {
28
+ group: 'For ' + this.id + ' Visualizer',
29
+ type: 'number',
30
+ description: '六角形の線の太さ',
31
+ default: 1
32
+ }).option(this.argKey('MaxOpacity'), {
33
+ group: 'For ' + this.id + ' Visualizer',
34
+ type: 'number',
35
+ description: '正規化後の最大塗り透明度',
36
+ default: 0.9
37
+ }).option(this.argKey('MinOpacity'), {
38
+ group: 'For ' + this.id + ' Visualizer',
39
+ type: 'number',
40
+ description: '正規化後の最小塗り透明度',
41
+ default: 0.5
42
+ });
43
+ }
44
+
45
+ getFutureCollection(result, target, visOptions) {
46
+ const hexIndex = {};
47
+
48
+ if (target && target.hex && Array.isArray(target.hex.features)) {
49
+ for (const hexF of target.hex.features) {
50
+ const id = hexF.properties && hexF.properties.hexId;
51
+ if (id != null) hexIndex[id] = hexF;
52
+ }
53
+ }
54
+
55
+ // Build index of triangles by triangleId
56
+ const triIndex = {};
57
+ if (target && target.triangles && Array.isArray(target.triangles.features)) {
58
+ for (const triF of target.triangles.features) {
59
+ const triId = triF.properties && triF.properties.triangleId;
60
+ if (triId != null) triIndex[triId] = triF;
61
+ }
62
+ }
63
+
64
+ const hexDataList = [];
65
+ let maxTotal = 0;
66
+ let globalMaxCategoryCount = 0; // Track max category count across entire grid
67
+
68
+ for (const hexId in result) {
69
+ const cats = result[hexId] || {};
70
+ let total = 0;
71
+ let maxCat = null;
72
+ let maxCount = -1;
73
+
74
+ for (const cat in cats) {
75
+ const count = (cats[cat]?.items?.features?.length) ?? 0;
76
+ total += count;
77
+ if (count > maxCount) {
78
+ maxCount = count;
79
+ maxCat = cat;
80
+ }
81
+ // Track global max
82
+ if (count > globalMaxCategoryCount) {
83
+ globalMaxCategoryCount = count;
84
+ }
85
+ }
86
+
87
+ if (total > 0) {
88
+ hexDataList.push({ hexId, cats, maxCat, maxCount, total });
89
+ if (total > maxTotal) {
90
+ maxTotal = total;
91
+ }
92
+ }
93
+ }
94
+
95
+ const out = { hex: [], triangles: [] };
96
+ const maxOp = (visOptions && visOptions.MaxOpacity) ?? 0.8;
97
+ const minOp = (visOptions && visOptions.MinOpacity) ?? 0.1;
98
+ const opRange = maxOp - minOp;
99
+
100
+ for (const hexData of hexDataList) {
101
+ const hexFeature = hexIndex[hexData.hexId];
102
+ if (!hexFeature) continue;
103
+
104
+ const f = JSON.parse(JSON.stringify(hexFeature));
105
+ f.properties = f.properties || {};
106
+ f.properties.majorityCategory = hexData.maxCat;
107
+ f.properties.majorityCount = hexData.maxCount;
108
+ f.properties.totalCount = hexData.total;
109
+
110
+ const normalizedOpacity = maxTotal > 0
111
+ ? minOp + (hexData.total / maxTotal) * opRange
112
+ : minOp;
113
+
114
+ const palette = target?.splatonePalette || {};
115
+ const pal = hexData.maxCat ? (palette[hexData.maxCat] || {}) : {};
116
+ f.properties.fill = true;
117
+ f.properties.fillColor = pal.color || '#888888';
118
+ f.properties.fillOpacity = normalizedOpacity;
119
+ f.properties.color = pal.darken || '#333333';
120
+ f.properties.weight = (visOptions && visOptions.HexWeight) ?? 1;
121
+ f.properties.opacity = (visOptions && visOptions.HexOpacity) ?? 1;
122
+
123
+ if (visOptions && visOptions.Hexapartite) {
124
+ f.properties.hexPartite = true;
125
+ f.properties.breakdown = Object.fromEntries(
126
+ Object.entries(hexData.cats).map(([c, v]) => [c, (v?.items?.features?.length) ?? 0])
127
+ );
128
+ f.properties.categoryColors = Object.fromEntries(
129
+ Object.entries(hexData.cats).map(([c]) => [c, palette[c]?.color || '#888888'])
130
+ );
131
+ // Store global max for opacity normalization in web.js fallback
132
+ f.properties.globalMaxCategoryCount = globalMaxCategoryCount;
133
+ }
134
+
135
+ out.hex.push(f);
136
+
137
+ // Add triangles for Hexapartite mode
138
+ if (visOptions && visOptions.Hexapartite && hexFeature.properties && hexFeature.properties.triIds) {
139
+ const triIds = hexFeature.properties.triIds;
140
+ // Extract numeric counts from category objects
141
+ const catEntries = Object.entries(hexData.cats).map(([cat, catObj]) => [cat, catObj.total]).sort((a, b) => b[1] - a[1]);
142
+ const total = catEntries.reduce((sum, [_, count]) => sum + count, 0);
143
+
144
+ // Compute slices for each category: ratio < 1/6 => 0 slices, 1/6 <= ratio < 2/6 => 1 slice, etc.
145
+ // Also store in which order categories should be placed (clockwise from north)
146
+ const catSliceList = []; // [{category, count, sliceCount, opacity}, ...]
147
+ let totalSlices = 0;
148
+
149
+ // First pass: allocate slices using floor
150
+ for (const [category, count] of catEntries) {
151
+ const ratio = total > 0 ? count / total : 0;
152
+ const sliceCount = Math.floor(ratio * 6); // 0-6 slices
153
+ // Compute opacity based on GLOBAL max category count:
154
+ // MinOpacity + (count / globalMaxCategoryCount) * (MaxOpacity - MinOpacity)
155
+ const sliceOpacity = minOp + (count / Math.max(globalMaxCategoryCount, 1)) * opRange;
156
+ catSliceList.push({ category, count, sliceCount, opacity: sliceOpacity });
157
+ totalSlices += sliceCount;
158
+ }
159
+
160
+ // Second pass: distribute remaining slices (6 - totalSlices) to categories with largest remainders
161
+ if (totalSlices < 6) {
162
+ const remainders = catEntries.map(([category, count], idx) => {
163
+ const ratio = total > 0 ? count / total : 0;
164
+ const remainder = (ratio * 6) - Math.floor(ratio * 6);
165
+ return { idx, remainder };
166
+ }).sort((a, b) => b.remainder - a.remainder);
167
+
168
+ let remaining = 6 - totalSlices;
169
+ for (let i = 0; i < remainders.length && remaining > 0; i++) {
170
+ catSliceList[remainders[i].idx].sliceCount++;
171
+ remaining--;
172
+ }
173
+ }
174
+
175
+ // Arrange slices clockwise starting from top (triIdx 0 = north/top)
176
+ let triIdx = 0;
177
+ for (const catSlice of catSliceList) {
178
+ for (let slicePos = 0; slicePos < catSlice.sliceCount && triIdx < triIds.length; slicePos++) {
179
+ const triId = triIds[triIdx];
180
+ const triFeature = triIndex[triId];
181
+ if (!triFeature) {
182
+ triIdx++;
183
+ continue;
184
+ }
185
+
186
+ const triCopy = JSON.parse(JSON.stringify(triFeature));
187
+ triCopy.properties = triCopy.properties || {};
188
+ triCopy.properties.category = catSlice.category;
189
+ triCopy.properties.categoryCount = catSlice.count;
190
+ triCopy.properties.slicePosition = slicePos; // which slice of this category (0-indexed)
191
+ triCopy.properties.sliceCount = catSlice.sliceCount; // total slices for this category
192
+ triCopy.properties.parentHexId = hexData.hexId;
193
+ triCopy.properties.fill = true;
194
+ const catColor = palette[catSlice.category]?.color || '#888888';
195
+ triCopy.properties.fillColor = catColor;
196
+ triCopy.properties.fillOpacity = catSlice.opacity;
197
+ triCopy.properties.color = palette[catSlice.category]?.darken || '#333333';
198
+ triCopy.properties.weight = (visOptions && visOptions.HexWeight) ?? 1;
199
+ triCopy.properties.opacity = (visOptions && visOptions.HexOpacity) ?? 1;
200
+
201
+ out.triangles.push(triCopy);
202
+ triIdx++;
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ const outputCollections = {};
209
+ for (const [k, v] of Object.entries(out)) {
210
+ outputCollections[k] = featureCollection(v);
211
+ }
212
+ return outputCollections;
213
+ }
214
+ }
@@ -0,0 +1,251 @@
1
+ let booted = false;
2
+ // keep last received geojson so helper renderers can access triangles/outlines
3
+ let lastGeojson = null;
4
+ // keep last visOptions for opacity bounds
5
+ let lastVisOptions = {};
6
+ export default async function main(map, geojson, options = {}) {
7
+ if (booted) return;
8
+ booted = true;
9
+ const layers = {};
10
+ // stash geojson and visOptions for render helpers
11
+ lastGeojson = geojson;
12
+ lastVisOptions = (options && options.visOptions) || {};
13
+
14
+ // Render hex layer with Leaflet GeoJSON
15
+ if (geojson && geojson.hex) {
16
+ const isHexapartite = options.visOptions && options.visOptions.Hexapartite;
17
+
18
+ // If Hexapartite mode, render triangles directly
19
+ if (isHexapartite) {
20
+ // Create a group to hold triangles + optional outlines
21
+ const group = L.featureGroup();
22
+
23
+ if (geojson && geojson.triangles && geojson.triangles.features) {
24
+ const triLayer = L.geoJSON(geojson.triangles, {
25
+ style: (feature) => {
26
+ const fillColor = feature.properties?.fillColor || '#888888';
27
+ return {
28
+ fill: true,
29
+ fillColor: fillColor,
30
+ fillOpacity: feature.properties?.fillOpacity ?? 0.5,
31
+ color: feature.properties?.color || '#333333',
32
+ weight: feature.properties?.weight || 1,
33
+ opacity: feature.properties?.opacity ?? 1,
34
+ };
35
+ },
36
+ onEachFeature: (feature, layer) => {
37
+ const cat = feature.properties?.category || 'Unknown';
38
+ const count = feature.properties?.categoryCount ?? 0;
39
+ const popupText = `Category: ${cat}<br/>Count: ${count}`;
40
+ layer.bindPopup(popupText);
41
+ }
42
+ });
43
+ triLayer.addTo(group);
44
+ }
45
+
46
+ // optionally add hex outlines so user can see hex borders
47
+ if (geojson && geojson.hex && geojson.hex.features) {
48
+ const outline = L.geoJSON(geojson.hex, {
49
+ style: (feature) => ({
50
+ color: feature.properties.color || '#333333',
51
+ weight: feature.properties.weight || 1,
52
+ opacity: feature.properties.opacity ?? 1,
53
+ fill: false
54
+ })
55
+ });
56
+ outline.addTo(group);
57
+ }
58
+
59
+ group.addTo(map);
60
+ layers['[MajorityHex]'] = group;
61
+ } else {
62
+ // Standard solid color hex rendering
63
+ const hexLayer = renderHexLayer(map, geojson.hex);
64
+ if (hexLayer) layers['[MajorityHex]'] = hexLayer;
65
+ }
66
+ }
67
+
68
+ return layers;
69
+ }
70
+
71
+ /**
72
+ * Render standard hex layer (solid color)
73
+ */
74
+ function renderHexLayer(map, hexFeatureCollection) {
75
+ const layer = L.geoJSON(hexFeatureCollection, {
76
+ style: (feature) => {
77
+ return {
78
+ color: feature.properties.color,
79
+ weight: feature.properties.weight,
80
+ opacity: feature.properties.opacity,
81
+ fill: feature.properties.fill,
82
+ fillColor: feature.properties.fillColor,
83
+ fillOpacity: feature.properties.fillOpacity
84
+ };
85
+ },
86
+ onEachFeature: (feature, layer) => {
87
+ const majorityCategory = feature.properties.majorityCategory;
88
+ const totalCount = feature.properties.totalCount;
89
+ const majorityCount = feature.properties.majorityCount;
90
+ const html = `<strong>${majorityCategory}</strong><br/>Data: ${majorityCount}/${totalCount}`;
91
+ layer.bindTooltip(html, { direction: 'top', opacity: 0.9 });
92
+ }
93
+ });
94
+ layer.addTo(map);
95
+ return layer;
96
+ }
97
+
98
+ /**
99
+ * Render Hexapartite layer: 6-slice pie chart representation using pre-computed triangles
100
+ */
101
+ function renderHexPartiteLayer(map, hexFeatureCollection) {
102
+ // Build a FeatureGroup containing pre-computed triangle slices (if available)
103
+ // plus optional hex outlines/tooltips. Do NOT add to map here; return the group so
104
+ // callers can add it to the map or layer control.
105
+ // Return a Leaflet layer (FeatureGroup). This avoids passing booleans to
106
+ // layer controls elsewhere. If precomputed triangles are not available
107
+ // (they are rendered in main when present), fall back to creating
108
+ // center-to-edge triangles per hex so the UI still shows a Hexapartite view.
109
+ const group = L.featureGroup();
110
+
111
+ if (!hexFeatureCollection || !Array.isArray(hexFeatureCollection.features)) {
112
+ return group;
113
+ }
114
+
115
+ const features = hexFeatureCollection.features;
116
+
117
+ for (const feature of features) {
118
+ const breakdown = feature.properties?.breakdown || {};
119
+ const categoryColors = feature.properties?.categoryColors || {};
120
+ const hexOutlineColor = feature.properties?.color || '#333333';
121
+ const hexOutlineWeight = feature.properties?.weight || 1;
122
+ const hexOutlineOpacity = feature.properties?.opacity ?? 1;
123
+
124
+ const coords = feature.geometry?.coordinates?.[0];
125
+ if (!coords || coords.length < 3) continue;
126
+
127
+ // normalize ring
128
+ const last = coords[coords.length - 1];
129
+ const first = coords[0];
130
+ const closed = last && first && last[0] === first[0] && last[1] === first[1];
131
+ const n = closed ? coords.length - 1 : coords.length;
132
+
133
+ // centroid
134
+ let sumLng = 0, sumLat = 0;
135
+ for (let i = 0; i < n; i++) {
136
+ sumLng += coords[i][0];
137
+ sumLat += coords[i][1];
138
+ }
139
+ const center = [sumLng / n, sumLat / n];
140
+
141
+ const catEntries = Object.entries(breakdown).sort((a, b) => b[1] - a[1]);
142
+ const total = catEntries.reduce((s, [, c]) => s + c, 0);
143
+ if (total === 0) continue;
144
+
145
+ // Get opacity bounds from lastVisOptions (same as node.js)
146
+ const minOp = (lastVisOptions && lastVisOptions.MinOpacity) ?? 0.1;
147
+ const maxOp = (lastVisOptions && lastVisOptions.MaxOpacity) ?? 0.8;
148
+ const opRange = maxOp - minOp;
149
+
150
+ // Get global max count from hex properties (set by node.js)
151
+ const globalMaxCount = feature.properties?.globalMaxCategoryCount ?? 0;
152
+
153
+ // Compute slice allocation per category: ratio < 1/6 => 0, 1/6 <= ratio < 2/6 => 1, etc.
154
+ const catSliceList = [];
155
+ let totalSlices = 0;
156
+
157
+ for (const [category, count] of catEntries) {
158
+ const ratio = total > 0 ? count / total : 0;
159
+ const sliceCount = Math.floor(ratio * 6);
160
+ if (sliceCount > 0) {
161
+ // Opacity based on GLOBAL max category count (same as node.js)
162
+ const sliceOpacity = minOp + (count / Math.max(globalMaxCount, 1)) * opRange;
163
+ catSliceList.push({ category, count, sliceCount, opacity: sliceOpacity });
164
+ totalSlices += sliceCount;
165
+ }
166
+ }
167
+
168
+ // Normalize if totalSlices > 6
169
+ if (totalSlices > 6) {
170
+ const scale = 6 / totalSlices;
171
+ let allottedSlices = 0;
172
+ for (let i = 0; i < catSliceList.length; i++) {
173
+ const newCount = Math.floor(catSliceList[i].sliceCount * scale);
174
+ allottedSlices += newCount;
175
+ catSliceList[i].sliceCount = newCount;
176
+ }
177
+ let remaining = 6 - allottedSlices;
178
+ for (let i = 0; i < catSliceList.length && remaining > 0; i++) {
179
+ if (catSliceList[i].sliceCount < Math.ceil(catSliceList[i].count / total * 6)) {
180
+ catSliceList[i].sliceCount++;
181
+ remaining--;
182
+ }
183
+ }
184
+ }
185
+
186
+ // Create triangles in clockwise order starting from north (triIdx 0)
187
+ let triIdx = 0;
188
+ for (const catSlice of catSliceList) {
189
+ for (let slicePos = 0; slicePos < catSlice.sliceCount && triIdx < n; slicePos++) {
190
+ const v1 = coords[triIdx];
191
+ const v2 = coords[(triIdx + 1) % n];
192
+
193
+ const triangle = {
194
+ type: 'Feature',
195
+ geometry: {
196
+ type: 'Polygon',
197
+ coordinates: [[[center[0], center[1]], [v1[0], v1[1]], [v2[0], v2[1]], [center[0], center[1]]]]
198
+ },
199
+ properties: {
200
+ category: catSlice.category,
201
+ categoryCount: catSlice.count,
202
+ slicePosition: slicePos,
203
+ sliceCount: catSlice.sliceCount,
204
+ fill: true,
205
+ fillColor: categoryColors[catSlice.category] || '#888888',
206
+ fillOpacity: catSlice.opacity,
207
+ color: hexOutlineColor,
208
+ weight: 0.5,
209
+ opacity: hexOutlineOpacity
210
+ }
211
+ };
212
+
213
+ L.geoJSON(triangle, {
214
+ style: (f) => ({
215
+ fill: true,
216
+ fillColor: f.properties.fillColor,
217
+ fillOpacity: f.properties.fillOpacity,
218
+ color: f.properties.color,
219
+ weight: f.properties.weight,
220
+ opacity: f.properties.opacity
221
+ })
222
+ }).addTo(group);
223
+
224
+ triIdx++;
225
+ }
226
+ }
227
+ }
228
+
229
+ // add outlines (non-filled) for all hexes so borders are visible
230
+ const outline = L.geoJSON(hexFeatureCollection, {
231
+ style: (feature) => ({
232
+ color: feature.properties.color || '#333333',
233
+ weight: feature.properties.weight || 1,
234
+ opacity: feature.properties.opacity ?? 1,
235
+ fill: false
236
+ }),
237
+ onEachFeature: (feature, layer) => {
238
+ const majorityCategory = feature.properties?.majorityCategory;
239
+ const totalCount = feature.properties?.totalCount;
240
+ const majorityCount = feature.properties?.majorityCount;
241
+ const html = `<strong>${majorityCategory}</strong><br/>Data: ${majorityCount}/${totalCount}`;
242
+ layer.bindTooltip(html, { direction: 'top', opacity: 0.9 });
243
+ }
244
+ });
245
+ outline.addTo(group);
246
+
247
+ group.addTo(map);
248
+ return group;
249
+
250
+
251
+ }
@@ -14,7 +14,16 @@ export default class MarkerClusterVisualizer extends VisualizerBase {
14
14
  super();
15
15
  this.id = path.basename(path.dirname(fileURLToPath(import.meta.url)));//必須(ディレクトリ名がビジュアライザ名)
16
16
  }
17
-
17
+ async yargv(yargv) {
18
+ // 必須項目にすると、このプラグインを使用しない時も必須になります。
19
+ // 必須項目は作らず、もしプラグインを使う上での制約違反はinitで例外を投げてください。
20
+ return yargv.option(this.argKey('MaxClusterRadius'), {
21
+ group: 'For ' + this.id + ' Visualizer',
22
+ type: 'number',
23
+ description: 'クラスタを構成する範囲(半径)',
24
+ default: 80
25
+ });
26
+ }
18
27
 
19
28
  concatFC(fcA, fcB) {
20
29
  return {
@@ -3,7 +3,7 @@ export default async function main(map, geojson, options = { palette: {}, visOpt
3
3
  if (booted) return;
4
4
  booted = true;
5
5
 
6
- console.log("[VIS OPTIONS]",options.visOptions);
6
+ console.log("[VIS OPTIONS]", options.visOptions);
7
7
 
8
8
  const urls = [
9
9
  'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css',
@@ -35,7 +35,7 @@ console.log("[VIS OPTIONS]",options.visOptions);
35
35
  const group = L.markerClusterGroup({
36
36
  chunkedLoading: true,
37
37
  disableClusteringAtZoom: 18,
38
- maxClusterRadius: 60,
38
+ maxClusterRadius: options.visOptions.MaxClusterRadius,
39
39
  spiderfyOnMaxZoom: true,
40
40
  showCoverageOnHover: false,
41
41
  iconCreateFunction: (cluster) => {
Binary file