splatone 0.0.16 → 0.0.18
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 +44 -75
- package/assets/screenshot_venice_heat.png +0 -0
- package/package.json +1 -1
- package/visualizer/heat/node.js +82 -0
- package/visualizer/heat/web.js +132 -0
package/README.md
CHANGED
|
@@ -10,9 +10,15 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
|
|
|
10
10
|
|
|
11
11
|
- Bulky: クロールした全てのジオタグを小さな点で描画する
|
|
12
12
|
- Marker Cluster: 密集しているジオタグをクラスタリングしてまとめて表示する
|
|
13
|
+
- Majority Hex: HexGridの各セルをセル内で最頻出するカテゴリの色で彩色
|
|
14
|
+
- Heat: ヒートマップ
|
|
13
15
|
|
|
14
16
|
## Change Log
|
|
15
17
|
|
|
18
|
+
### v0.0.17 → v0.0.18
|
|
19
|
+
|
|
20
|
+
* **[可視化モジュール]** ```--vis-heat```追加
|
|
21
|
+
|
|
16
22
|
### v0.0.13 → v0.0.14 → v0.0.15 → v0.0.16 → v0.0.17
|
|
17
23
|
* **[可視化モジュール]** ```--vis-majority-hex```追加
|
|
18
24
|
* 結果の色固定機能追加 (キーワード指定方法を参照の事)
|
|
@@ -37,71 +43,6 @@ $ npx -y -p splatone@latest crawler --help
|
|
|
37
43
|
[app] [plugin] loaded: flickr@1.0.0
|
|
38
44
|
使い方: crawler.js [options]
|
|
39
45
|
|
|
40
|
-
Basic Options
|
|
41
|
-
-p, --plugin 実行するプラグイン[文字列] [必須] [選択してください: "flickr"]
|
|
42
|
-
-k, --keywords 検索キーワード(|区切り) [文字列] [デフォルト:
|
|
43
|
-
"nature,tree,flower|building,house|water,sea,river,pond"]
|
|
44
|
-
-f, --filed 大きなデータをファイルとして送受信する
|
|
45
|
-
[真偽] [デフォルト: true]
|
|
46
|
-
-c, --chopped 大きなデータを細分化して送受信する
|
|
47
|
-
[非推奨] [真偽] [デフォルト: false]
|
|
48
|
-
|
|
49
|
-
Debug
|
|
50
|
-
--debug-verbose デバッグ情報出力 [真偽] [デフォルト: false]
|
|
51
|
-
|
|
52
|
-
For flickr Plugin
|
|
53
|
-
--p-flickr-APIKEY Flickr ServiceのAPI KEY [文字列]
|
|
54
|
-
--p-flickr-Extras カンマ区切り/保持する写真のメタデータ(デフォルト値は
|
|
55
|
-
記載の有無に関わらず保持)
|
|
56
|
-
[文字列] [デフォルト: "date_upload,date_taken,owner_name,geo,url_s,tags"]
|
|
57
|
-
--p-flickr-DateMode 利用時間軸(update=Flickr投稿日時/taken=写真撮影日時)
|
|
58
|
-
[選択してください: "upload", "taken"] [デフォルト: "upload"]
|
|
59
|
-
--p-flickr-Haste 時間軸分割並列処理 [真偽] [デフォルト: true]
|
|
60
|
-
--p-flickr-DateMax クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD
|
|
61
|
-
[文字列] [デフォルト: 1763107393]
|
|
62
|
-
--p-flickr-DateMin クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD
|
|
63
|
-
[文字列] [デフォルト: 1072882800]
|
|
64
|
-
|
|
65
|
-
Visualization (最低一つの指定が必須です)
|
|
66
|
-
--vis-bulky 全データをCircleMarkerとして地図上に表示
|
|
67
|
-
[真偽] [デフォルト: false]
|
|
68
|
-
--vis-majority-hex HexGrid内で最も出現頻度が高いカテゴリの色で彩色。Hex
|
|
69
|
-
apartiteモードで6分割パイチャート表示。透明度は全体
|
|
70
|
-
で正規化。 [真偽] [デフォルト: false]
|
|
71
|
-
--vis-marker-cluster マーカークラスターとして地図上に表示
|
|
72
|
-
[真偽] [デフォルト: false]
|
|
73
|
-
|
|
74
|
-
For bulky Visualizer
|
|
75
|
-
--v-bulky-Radius Point Markerの半径 [数値] [デフォルト: 5]
|
|
76
|
-
--v-bulky-Stroke Point Markerの線の有無 [真偽] [デフォルト: true]
|
|
77
|
-
--v-bulky-Weight Point Markerの線の太さ [数値] [デフォルト: 1]
|
|
78
|
-
--v-bulky-Opacity Point Markerの線の透明度 [数値] [デフォルト: 1]
|
|
79
|
-
--v-bulky-Filling Point Markerの塗りの有無 [真偽] [デフォルト: true]
|
|
80
|
-
--v-bulky-FillOpacity Point Markerの塗りの透明度 [数値] [デフォルト: 0.5]
|
|
81
|
-
|
|
82
|
-
For majority-hex Visualizer
|
|
83
|
-
--v-majority-hex-Hexapartite 中のカテゴリの頻度に応じて六角形を分割色彩
|
|
84
|
-
[真偽] [デフォルト: false]
|
|
85
|
-
--v-majority-hex-HexOpacity 六角形の線の透明度 [数値] [デフォルト: 1]
|
|
86
|
-
--v-majority-hex-HexWeight 六角形の線の太さ [数値] [デフォルト: 1]
|
|
87
|
-
--v-majority-hex-MaxOpacity 正規化後の最大塗り透明度
|
|
88
|
-
[数値] [デフォルト: 0.9]
|
|
89
|
-
--v-majority-hex-MinOpacity 正規化後の最小塗り透明度
|
|
90
|
-
[数値] [デフォルト: 0.5]
|
|
91
|
-
|
|
92
|
-
For marker-cluster Visualizer
|
|
93
|
-
--v-marker-cluster-MaxClusterRadius クラスタを構成する範囲(半径)
|
|
94
|
-
[数値] [デフォルト: 80]
|
|
95
|
-
|
|
96
|
-
オプション:
|
|
97
|
-
--help ヘルプを表示 [真偽]
|
|
98
|
-
--version バージョンを表示 [真偽]
|
|
99
|
-
|
|
100
|
-
cold_@bimota-due MINGW64 /c/GitHub/Splatone (61-可視化メソッドmajorityhex)
|
|
101
|
-
$ npx -y -p crawler@latest --help
|
|
102
|
-
[app] [plugin] loaded: flickr@1.0.0
|
|
103
|
-
使い方: crawler.js [options]
|
|
104
|
-
|
|
105
46
|
Basic Options
|
|
106
47
|
-p, --plugin 実行するプラグイン[文字列] [必須] [選択してください: "flickr"]
|
|
107
48
|
-k, --keywords 検索キーワード(|区切り) [文字列] [デフォルト:
|
|
@@ -123,13 +64,15 @@ For flickr Plugin
|
|
|
123
64
|
[選択してください: "upload", "taken"] [デフォルト: "upload"]
|
|
124
65
|
--p-flickr-Haste 時間軸分割並列処理 [真偽] [デフォルト: true]
|
|
125
66
|
--p-flickr-DateMax クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD
|
|
126
|
-
[文字列] [デフォルト:
|
|
67
|
+
[文字列] [デフォルト: 1763224757]
|
|
127
68
|
--p-flickr-DateMin クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD
|
|
128
69
|
[文字列] [デフォルト: 1072882800]
|
|
129
70
|
|
|
130
71
|
Visualization (最低一つの指定が必須です)
|
|
131
72
|
--vis-bulky 全データをCircleMarkerとして地図上に表示
|
|
132
73
|
[真偽] [デフォルト: false]
|
|
74
|
+
--vis-heat カテゴリ毎に異なるレイヤのヒートマップで可視化(色=
|
|
75
|
+
カテゴリ色、透明度=頻度) [真偽] [デフォルト: false]
|
|
133
76
|
--vis-majority-hex HexGrid内で最も出現頻度が高いカテゴリの色で彩色。Hex
|
|
134
77
|
apartiteモードで6分割パイチャート表示。透明度は全体
|
|
135
78
|
で正規化。 [真偽] [デフォルト: false]
|
|
@@ -144,6 +87,13 @@ For bulky Visualizer
|
|
|
144
87
|
--v-bulky-Filling Point Markerの塗りの有無 [真偽] [デフォルト: true]
|
|
145
88
|
--v-bulky-FillOpacity Point Markerの塗りの透明度 [数値] [デフォルト: 0.5]
|
|
146
89
|
|
|
90
|
+
For heat Visualizer
|
|
91
|
+
--v-heat-Radius ヒートマップブラーの半径 [数値] [デフォルト: 0.0005]
|
|
92
|
+
--v-heat-MinOpacity ヒートマップの最小透明度 [数値] [デフォルト: 0]
|
|
93
|
+
--v-heat-MaxOpacity ヒートマップの最大透明度 [数値] [デフォルト: 1]
|
|
94
|
+
--v-heat-MaxValue ヒートマップ強度の最大値
|
|
95
|
+
(未指定時はデータから自動推定) [数値]
|
|
96
|
+
|
|
147
97
|
For majority-hex Visualizer
|
|
148
98
|
--v-majority-hex-Hexapartite 中のカテゴリの頻度に応じて六角形を分割色彩
|
|
149
99
|
[真偽] [デフォルト: false]
|
|
@@ -162,7 +112,6 @@ For marker-cluster Visualizer
|
|
|
162
112
|
--help ヘルプを表示 [真偽]
|
|
163
113
|
--version バージョンを表示 [真偽]
|
|
164
114
|
```
|
|
165
|
-
|
|
166
115
|
## 最小コマンド例
|
|
167
116
|
|
|
168
117
|
1. *plugin*を一つ、*visualizer*を一つ以上指定し、複数のキーワードでクロールを開始します。
|
|
@@ -176,7 +125,7 @@ For marker-cluster Visualizer
|
|
|
176
125
|

|
|
177
126
|
|
|
178
127
|
```bash
|
|
179
|
-
$ npx -y
|
|
128
|
+
$ npx -y -p splatone@latest crawler -p flickr -k "canal,river|street,alley|bridge" --vis-bulky --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
180
129
|
```
|
|
181
130
|
|
|
182
131
|
# 詳細説明
|
|
@@ -221,7 +170,7 @@ APIキーは以下の3種類の方法で与える事ができます
|
|
|
221
170
|
#### コマンド例
|
|
222
171
|
* クエリは海と山のキーワード検索。上記スクリーンショットは日本のデータ
|
|
223
172
|
```shell
|
|
224
|
-
$
|
|
173
|
+
$ npx -y -p splatone@latest crawler -p flickr -k "sea,ocean|mountain,mount" --vis-bulky--p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
225
174
|
```
|
|
226
175
|
|
|
227
176
|
#### コマンドライン引数
|
|
@@ -244,13 +193,33 @@ $ npx -y -p splatone@latest crawler -p flickr -k "sea,ocean|mountain,mount" --v
|
|
|
244
193
|
```shell
|
|
245
194
|
$ npx -y -p 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"
|
|
246
195
|
```
|
|
247
|
-
|
|
248
196
|
#### コマンドライン引数
|
|
249
197
|
|
|
250
198
|
| オプション | 説明 | 型 | デフォルト |
|
|
251
199
|
| :---------------------------------------- | :--------------------------- | :--- | :--------- |
|
|
252
200
|
| ```--v-marker-cluster-MaxClusterRadius``` | クラスタを構成する範囲(半径) | 数値 | 80 |
|
|
253
201
|
|
|
202
|
+
### Heat: ヒートマップ
|
|
203
|
+
|
|
204
|
+

|
|
205
|
+
|
|
206
|
+
#### コマンド例
|
|
207
|
+
|
|
208
|
+
* クエリは水域・緑地・交通・ランドマークを色分けしたもの。上記スクリーンショットはフロリダ半島全体
|
|
209
|
+
|
|
210
|
+
```shell
|
|
211
|
+
$ npx -y -p splatone@latest crawler -p flickr -k "canal,river|street,alley|bridge" --vis-heat --p-flickr-APIKEY="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### コマンドライン引数
|
|
215
|
+
|
|
216
|
+
| オプション | 説明 | 型 | デフォルト |
|
|
217
|
+
| :------------------------ | :----------------------------------------------------- | :--- | :--------- |
|
|
218
|
+
| ```--v-heat-Radius``` | ヒートマップブラーの半径 | 数値 | 0.0005 |
|
|
219
|
+
| ```--v-heat-MinOpacity``` | ヒートマップの最小透明度 | 数値 | 0 |
|
|
220
|
+
| ```--v-heat-MaxOpacity``` | ヒートマップの最大透明度 | 数値 | 1 |
|
|
221
|
+
| ```--v-heat-MaxValue``` | ヒートマップ強度の最大値(未指定時はデータから自動推定) | 数値 | |
|
|
222
|
+
|
|
254
223
|
### Majority Hex: Hexグリッド内の出現頻度に応じた彩色
|
|
255
224
|
|
|
256
225
|

|
|
@@ -259,7 +228,7 @@ $ npx -y -p splatone@latest crawler -p flickr -k "水域=canal,channel,waterway,
|
|
|
259
228
|
* クエリは水域・緑地・交通・ランドマークを色分けしたもの。上記スクリーンショットはフロリダ半島全体
|
|
260
229
|
*
|
|
261
230
|
```shell
|
|
262
|
-
$
|
|
231
|
+
$ 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"
|
|
263
232
|
```
|
|
264
233
|
|
|
265
234
|
#### コマンドライン引数
|
|
@@ -321,17 +290,17 @@ seaだけでは集められるポストが限定されるので、同様の意
|
|
|
321
290
|
- 使い方(6色のカラーパレットを2セット作りたい):
|
|
322
291
|
|
|
323
292
|
```bash
|
|
324
|
-
|
|
293
|
+
npx -y -p splatone@latest color <count> <sets>
|
|
325
294
|
# 例: 6色を3セット生成(ターミナルに色付きで表示)
|
|
326
|
-
|
|
327
|
-
```
|
|
295
|
+
npx -y -p splatone@latest color 6 3
|
|
296
|
+
```
|
|
328
297
|
|
|
329
298
|
- オプション:
|
|
330
299
|
|
|
331
300
|
- `--no-ansi` : ANSI カラーシーケンスを出力せず、プレーンなカンマ区切りの HEX を出力します(パイプやログ向け)。
|
|
332
301
|
|
|
333
302
|
```bash
|
|
334
|
-
|
|
303
|
+
npx -y -p splatone@latest color --no-ansi 6 3
|
|
335
304
|
```
|
|
336
305
|
|
|
337
306
|
## ダウンロード
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
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 HeatVisualizer extends VisualizerBase {
|
|
7
|
+
static name = 'Heat Visualizer';
|
|
8
|
+
static version = '0.0.0';
|
|
9
|
+
static description = "カテゴリ毎に異なるレイヤのヒートマップで可視化(色=カテゴリ色、透明度=頻度)";
|
|
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('Radius'), {
|
|
18
|
+
group: 'For ' + this.id + ' Visualizer',
|
|
19
|
+
type: 'number',
|
|
20
|
+
description: 'ヒートマップブラーの半径',
|
|
21
|
+
default: 0.0005
|
|
22
|
+
}).option(this.argKey('MinOpacity'), {
|
|
23
|
+
group: 'For ' + this.id + ' Visualizer',
|
|
24
|
+
type: 'number',
|
|
25
|
+
description: 'ヒートマップの最小透明度',
|
|
26
|
+
default: 0
|
|
27
|
+
}).option(this.argKey('MaxOpacity'), {
|
|
28
|
+
group: 'For ' + this.id + ' Visualizer',
|
|
29
|
+
type: 'number',
|
|
30
|
+
description: 'ヒートマップの最大透明度',
|
|
31
|
+
default: 1
|
|
32
|
+
}).option(this.argKey('MaxValue'), {
|
|
33
|
+
group: 'For ' + this.id + ' Visualizer',
|
|
34
|
+
type: 'number',
|
|
35
|
+
description: 'ヒートマップ強度の最大値 (未指定時はデータから自動推定)'
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getFutureCollection(result, target, visOptions) {
|
|
40
|
+
// result: { hexId: { category: { items, ids, final, crawled, total }, ... }, ... }
|
|
41
|
+
// target: { hex, triangles, categories, splatonePalette }
|
|
42
|
+
// Build category-based heatmap layers using individual data points (not hex centroids)
|
|
43
|
+
|
|
44
|
+
const categories = {};
|
|
45
|
+
|
|
46
|
+
// Iterate through all hexes and categories, collecting individual point features
|
|
47
|
+
for (const hexId in result) {
|
|
48
|
+
const hexData = result[hexId];
|
|
49
|
+
for (const cat in hexData) {
|
|
50
|
+
if (!categories[cat]) {
|
|
51
|
+
categories[cat] = [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get the actual items (point features) for this category in this hex
|
|
55
|
+
const items = hexData[cat].items;
|
|
56
|
+
if (!items || !items.features || items.features.length === 0) continue;
|
|
57
|
+
|
|
58
|
+
// Add each individual point feature to the category collection
|
|
59
|
+
for (const feature of items.features) {
|
|
60
|
+
const density = hexData[cat]?.total ?? items.features.length ?? 1;
|
|
61
|
+
// Clone the feature and add category property
|
|
62
|
+
const pointFeature = {
|
|
63
|
+
type: 'Feature',
|
|
64
|
+
geometry: feature.geometry,
|
|
65
|
+
properties: {
|
|
66
|
+
...feature.properties,
|
|
67
|
+
category: cat,
|
|
68
|
+
hexId: hexId,
|
|
69
|
+
hexDensity: density
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
categories[cat].push(pointFeature);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Return FeatureCollections per category
|
|
78
|
+
return Object.fromEntries(
|
|
79
|
+
Object.entries(categories).map(([k, v]) => [k, featureCollection(v)])
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Heat visualizer dependencies
|
|
2
|
+
const dependencies = [
|
|
3
|
+
{ type: 'script', src: 'https://cdn.jsdelivr.net/npm/heatmap.js@2.0.5/build/heatmap.min.js' },
|
|
4
|
+
{ type: 'script', src: 'https://raw.githack.com/pa7/heatmap.js/develop/plugins/leaflet-heatmap/leaflet-heatmap.js' }
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
// Load external dependencies dynamically
|
|
8
|
+
async function loadDependencies() {
|
|
9
|
+
for (const dep of dependencies) {
|
|
10
|
+
if (dep.type === 'script') {
|
|
11
|
+
// Check if already loaded
|
|
12
|
+
const existing = document.querySelector(`script[src="${dep.src}"]`);
|
|
13
|
+
if (existing) continue;
|
|
14
|
+
|
|
15
|
+
await new Promise((resolve, reject) => {
|
|
16
|
+
const script = document.createElement('script');
|
|
17
|
+
script.src = dep.src;
|
|
18
|
+
script.onload = resolve;
|
|
19
|
+
script.onerror = reject;
|
|
20
|
+
document.head.appendChild(script);
|
|
21
|
+
});
|
|
22
|
+
} else if (dep.type === 'link') {
|
|
23
|
+
// Check if already loaded
|
|
24
|
+
const existing = document.querySelector(`link[href="${dep.src}"]`);
|
|
25
|
+
if (existing) continue;
|
|
26
|
+
|
|
27
|
+
const link = document.createElement('link');
|
|
28
|
+
link.rel = 'stylesheet';
|
|
29
|
+
link.href = dep.src;
|
|
30
|
+
document.head.appendChild(link);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let booted = false;
|
|
36
|
+
export default async function main(map, geojson, options = {}) {
|
|
37
|
+
console.log("[VIS OPTIONS]", options.visOptions);
|
|
38
|
+
if (booted) return;
|
|
39
|
+
booted = true;
|
|
40
|
+
|
|
41
|
+
// Load dependencies first
|
|
42
|
+
await loadDependencies();
|
|
43
|
+
|
|
44
|
+
const layers = {};
|
|
45
|
+
const visOpts = options.visOptions || {};
|
|
46
|
+
|
|
47
|
+
// Extract category colors from palette (if available)
|
|
48
|
+
const palette = options.palette || {};
|
|
49
|
+
|
|
50
|
+
for (const cat in geojson) {
|
|
51
|
+
const features = geojson[cat].features || [];
|
|
52
|
+
if (features.length === 0) continue;
|
|
53
|
+
|
|
54
|
+
// Convert features to heatmap data format: { lat, lng, value }
|
|
55
|
+
const heatmapData = features.map(f => {
|
|
56
|
+
const coords = f.geometry.coordinates; // [lon, lat]
|
|
57
|
+
const rawValue = Number(f.properties?.weight ?? f.properties?.count ?? 1);
|
|
58
|
+
const value = Number.isFinite(rawValue) && rawValue > 0 ? rawValue : 1;
|
|
59
|
+
return {
|
|
60
|
+
lat: coords[1],
|
|
61
|
+
lng: coords[0],
|
|
62
|
+
value
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Estimate density-based max using originating hex totals as hints
|
|
67
|
+
const densityHints = features
|
|
68
|
+
.map(f => Number(f.properties?.hexDensity))
|
|
69
|
+
.filter(v => Number.isFinite(v) && v > 0);
|
|
70
|
+
|
|
71
|
+
const autoMax = densityHints.length > 0
|
|
72
|
+
? Math.max(...densityHints)
|
|
73
|
+
: Math.max(features.length, 1);
|
|
74
|
+
|
|
75
|
+
const configuredMax = Number(visOpts.MaxValue);
|
|
76
|
+
const maxValue = Number.isFinite(configuredMax) && configuredMax > 0
|
|
77
|
+
? configuredMax
|
|
78
|
+
: autoMax;
|
|
79
|
+
|
|
80
|
+
// Get category color from palette (fallback to blue)
|
|
81
|
+
const categoryColor = palette[cat]?.color || '#3388ff';
|
|
82
|
+
|
|
83
|
+
// Create gradient using category color
|
|
84
|
+
// Convert hex to RGB for gradient stops
|
|
85
|
+
const hexToRgb = (hex) => {
|
|
86
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
87
|
+
return result ? {
|
|
88
|
+
r: parseInt(result[1], 16),
|
|
89
|
+
g: parseInt(result[2], 16),
|
|
90
|
+
b: parseInt(result[3], 16)
|
|
91
|
+
} : { r: 51, g: 136, b: 255 };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const rgb = hexToRgb(categoryColor);
|
|
95
|
+
const gradient = {
|
|
96
|
+
0.0: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.0)`,
|
|
97
|
+
0.2: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.1)`,
|
|
98
|
+
0.4: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.4)`,
|
|
99
|
+
0.6: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.7)`,
|
|
100
|
+
0.8: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.9)`,
|
|
101
|
+
1.0: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 1.0)`
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Create heatmap layer configuration
|
|
105
|
+
const cfg = {
|
|
106
|
+
radius: visOpts.Radius || 25,
|
|
107
|
+
maxOpacity: visOpts.MaxOpacity || 0.8,
|
|
108
|
+
minOpacity: visOpts.MinOpacity || 0.1,
|
|
109
|
+
scaleRadius: true,
|
|
110
|
+
useLocalExtrema: false,
|
|
111
|
+
latField: 'lat',
|
|
112
|
+
lngField: 'lng',
|
|
113
|
+
valueField: 'value',
|
|
114
|
+
gradient: gradient
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Create HeatmapOverlay instance
|
|
118
|
+
const heatmapLayer = new HeatmapOverlay(cfg);
|
|
119
|
+
|
|
120
|
+
// Set data
|
|
121
|
+
heatmapLayer.setData({
|
|
122
|
+
max: maxValue,
|
|
123
|
+
data: heatmapData
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Add to map and store reference
|
|
127
|
+
heatmapLayer.addTo(map);
|
|
128
|
+
layers[cat] = heatmapLayer;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return layers;
|
|
132
|
+
}
|