splatone 0.0.2 → 0.0.3

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
@@ -1,33 +1,153 @@
1
- # Splatone - Multi-layer Composite Heatmap
2
-
3
- # 概要
4
-
5
- SNSのジオタグ付きポストを収集するツールです。現在は以下のSNSに対応しています。
6
-
7
- - Flickr
8
-
9
- 集めたデータは保存できる他、地図上で可視化する事が出来ます。以下の可視化に対応しています。
10
-
11
- - Bulky: クロールした全てのジオタグを小さな点で描画する
12
- - Marker Cluster: 密集しているジオタグをクラスタリングしてまとめて表示する
13
-
14
- # 使い方
15
-
16
- - [Node.js](https://nodejs.org/ja/download)をインストール後、NPXで実行します。
17
-
18
-
19
- ## Helpの表示
20
-
21
- ```
22
- nxp -- splatone@latest --help
23
- ```
24
- ## クローリングの実行
25
-
26
- - 以下のサンプルコマンドを参考に実行してください。(FlickrのAPIキーは自身のに置き換える事)
27
-
28
- ```
29
- 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" --vB
30
- ```
31
-
32
- - ブラウザが立ち上がるので地図上でポリゴンあるいは矩形で領域選択し、実行ボタンを押すとクロールが開始されます。
33
- - 指定した範囲を内包するHexGrid(六角形グリッド)が生成され、その内側のみが収集されます。
1
+ # Splatone - Multi-layer Composite Heatmap
2
+
3
+ # 概要
4
+
5
+ SNSのジオタグ付きポストをキーワードに基づいて収集するツールです。キーワードは複数指定し、それぞれのキーワードの出現分布を地図上にマップします。現在は以下のSNSに対応しています。
6
+
7
+ - Flickr
8
+
9
+ 集めたデータはキーワード毎に色分けされ地図上で可視化されます。以下の可視化手法に対応しています。
10
+
11
+ - Bulky: クロールした全てのジオタグを小さな点で描画する
12
+ - Marker Cluster: 密集しているジオタグをクラスタリングしてまとめて表示する
13
+
14
+ ## 既知のバグ
15
+
16
+ - JSON.stringify(json)で変換できる大きさに制限があり、数十万件等の大きな結果を生み出すクエリは、クロール後、結果のブラウザへの転送で失敗します。
17
+
18
+ # 使い方
19
+
20
+ - [Node.js](https://nodejs.org/ja/download)をインストール後、npxで実行します。
21
+ - npxはnpm上のモジュールをコマンド一つでインストールと実行を行う事ができるコマンドです。
22
+
23
+ ## Helpの表示
24
+
25
+ ```shell
26
+ $ npx -y -- splatone@latest crawler --help
27
+ 使い方: crawler.js [options]
28
+
29
+ Basic Options
30
+ -p, --plugin 実行するプラグイン
31
+ [文字列] [必須] [選択してください: "flickr"]
32
+ -o, --options プラグインオプション [文字列] [デフォルト: "{}"]
33
+ -k, --keywords 検索キーワード(|区切り) [文字列] [デフォルト:
34
+ "nature,tree,flower|building,house|water,sea,river,pond"]
35
+
36
+ Visualization (最低一つの指定が必須です)
37
+ --vis-bulky 全データをCircleMarkerとして地図上に表示
38
+ [真偽] [デフォルト: false]
39
+ --vis-marker-cluster マーカークラスターとして地図上に表示
40
+ [真偽] [デフォルト: false]
41
+
42
+ オプション:
43
+ --help ヘルプを表示 [真偽]
44
+ --version バージョンを表示 [真偽]
45
+ ```
46
+ ## クローリングの実行
47
+
48
+ - 以下のサンプルコマンドを参考に実行してください。
49
+ - **FlickrのAPIキーは自身のに置き換える事**
50
+ - ブラウザが立ち上がるので地図上でポリゴンあるいは矩形で領域選択し、実行ボタンを押すとクロールが開始されます。
51
+ - 指定した範囲を内包するHexGrid(六角形グリッド)が生成され、その内側のみが収集されます。
52
+ - 結果が表示された後、結果をGeoJSON形式でダウンロードできます。
53
+
54
+ ### 事例1) 商業施設・飲食施設・文化施設・公園の分類
55
+ ```shell
56
+ $ node crawler.js -p flickr -o '{"flickr":{"API_KEY":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}' -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
57
+ ```
58
+ - オプションの **--vis-bulky** を **--vis-marker-cluster** に変更する事でマーカークラスターで可視化できます。
59
+
60
+ ### 事例2)水路・陸路・ランドマーク等の分類
61
+ ```shell
62
+ $ node crawler.js -p flickr -o '{"flickr":{"API_KEY":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}' -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
63
+ ```
64
+ - ベネチア等の水路のある町でやると面白いです
65
+
66
+ # 詳細説明
67
+
68
+ ## APIキーの与え方
69
+
70
+ APIキーは以下の3種類の方法で与える事ができます
71
+ - ```--option```に含める
72
+ - 上記コマンド例の方法
73
+ - **flickr**の場合は``` -o '{"plugin":{"API_KEY":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}'```になります。
74
+ - [注意] コマンドを他の人と共有する時、APIキーをそのまま渡す事は危険です。
75
+ - **flickr**の場合は``` -o '{"flickr":{"API_KEY":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}'```になります。
76
+ - 環境変数で渡す
77
+ - ```API_KEY_plugin```という環境変数に格納する
78
+ - コマンドに毎回含めなくて良くなる。
79
+ - **flickr**の場合は```.API_KEY_flickr```になります。
80
+ - ```plugin```はプラグイン名(flickr等)に置き換えてください。
81
+ - 一時的な環境変数を定義する事も可能です。(bash等)
82
+ - ```API_KEY_flickr="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" node crawler.js -p flickr -k "sea,ocean|mountain,mount" --vis-bulky```
83
+ - ファイルで渡す(npxでは不可)
84
+ - ルートディレクトリに```.API_KEY.plugin```というファイルを作成し保存
85
+ - ```plugin```はプラグイン名(flickr等)に置き換えてください。
86
+ - **flickr**の場合は```.API_KEY.flickr```になります。
87
+ - optionや環境変数で与えるよりも優先されます。
88
+
89
+ ## Visualizer (可視化ツール)
90
+
91
+ ### Bulky: 全ての点を地図上にポイントする
92
+
93
+ ![](assets/screenshot_venice_bulky.png)
94
+
95
+ * クエリは水域と通路・橋梁・ランドマークを色分けしたもの、上記スクリーンショットはベネチア付近のデータ
96
+ ```shell
97
+ $ node crawler.js -p flickr -o '{"flickr":{"API_KEY":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}' -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
98
+ ```
99
+
100
+ ### Marker Cluster: 高密度の地点はマーカーをまとめて表示する
101
+ ![](assets/screenshot_venice_marker-cluster.png)
102
+
103
+ * クエリは水域と通路・橋梁・ランドマークを色分けしたもの、上記スクリーンショットはベネチア付近のデータ
104
+ ```shell
105
+ $ node crawler.js -p flickr -o '{"flickr":{"API_KEY":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}' -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-marker-cluster
106
+ ```
107
+ ## キーワード指定方法
108
+
109
+ ### 比較キーワードの指定
110
+
111
+ 複数のキーワードでジオタグ付きポストを集め分布を比較します。比較キーワードは「|」区切りで指定します。例えばseaとmountainの分布を調べたい場合は以下のようにします。この例では、seaとタグ付けられたポストとmountainとタグ付けられたポストが色分けされて分布を表示します。
112
+
113
+ ```
114
+ -k "sea|mountain"
115
+ ```
116
+
117
+ ### 類語キーワードの指定
118
+
119
+ seaだけでは集められるポストが限定されるので、同様の意味のキーワードも指定してor検索したいと考えるかもしれません。その場合は「,」で区切ってキーワードを並べる事ができます。これを類語キーワードと呼びます。例えばseaとocean、mountainとmountでor検索したい場合は以下のように指定します。
120
+
121
+ ```
122
+ -k "sea,ocean|mountain,mount"
123
+ ```
124
+
125
+ ### 実行例 (海岸線と山岳の分布)
126
+
127
+ ```shell
128
+ $ node crawler.js -p flickr -o '{"flickr":{"API_KEY":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}' -k "sea,ocean|mountain,mount" --vis-bulky
129
+ ```
130
+ ![](assets/screenshot_sea-mountain_bulky.png)
131
+
132
+ ### カテゴリ名の指定
133
+
134
+ 複数の類語キーワードを指定した場合、それらをまとめるカテゴリ名を付ける事ができます。たとえはsea,oceanに『海域』、mountain,mountに『山岳』とカテゴリ名をつけるには以下のように指定します。なお、指定は必須ではありません。指定しない場合はそれぞれ1番目のキーワード(seaとmountain)がカテゴリ名になります。
135
+
136
+ ```
137
+ -k "海域=sea,ocean|山岳=mountain,mount"
138
+ ```
139
+
140
+ ## ダウンロード
141
+
142
+ ### 画像のダウンロード
143
+
144
+ * 結果の地図を画像(PNG形式)としてダウンロードするには、画面右下のアイコンをクリックしてください。
145
+
146
+ ![](assets/icon_image_download.png)
147
+
148
+ ### データのダウンロード
149
+
150
+ * クロール結果をデータとしてダウンロードしたい場合は凡例の下にあるエクスポートボタンをクリックしてください。
151
+
152
+ ![](assets/icon_data_export.png)
153
+
Binary file
Binary file
package/crawler.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // -------------------------------
3
+ // -------------------------------
4
4
  // Node.js core (ESM)
5
5
  // -------------------------------
6
6
  import http from 'node:http';
@@ -19,8 +19,7 @@ import open from 'open';
19
19
  import Piscina from 'piscina';
20
20
  import uniqid from 'uniqid';
21
21
  import { Server as IOServer } from 'socket.io';
22
- import { centroid, featureCollection, hexGrid, polygon } from '@turf/turf';
23
- import booleanWithin from '@turf/boolean-within';
22
+ import { centroid, featureCollection, hexGrid, polygon, buffer, bboxPolygon, bbox as turfbbox, booleanIntersects } from '@turf/turf';
24
23
  import yargs from 'yargs';
25
24
  import { hideBin } from 'yargs/helpers';
26
25
 
@@ -107,14 +106,14 @@ try {
107
106
  });
108
107
  // コマンド例
109
108
  // 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
110
- // node crawler.js -p flickr -k "水域=canal,channel,waterway,river,stream,watercourse,sea,ocean,gulf,bay,strait,lagoon,offshore|橋梁=bridge,overpass,flyover,aqueduct,trestle|通路=street,road,thoroughfare,roadway,avenue,boulevard,lane,alley,roadway,carriageway,highway,motorway|ランドマーク=church,sanctuary,chapel,cathedral,basilica,minster,abbey" --vis-bulky
109
+ // node crawler.js -p flickr -k "水域=canal,channel,waterway,river,stream,watercourse,sea,ocean,gulf,bay,strait,lagoon,offshore|橋梁=bridge,overpass,flyover,aqueduct,trestle|通路=street,road,thoroughfare,roadway,avenue,boulevard,lane,alley,roadway,carriageway,highway,motorway|ランドマーク=church,sanctuary,chapel,cathedral,basilica,minster,abbey,temple,shrine" --vis-bulky
111
110
  let yargv = await yargs(hideBin(process.argv))
112
111
  .strict() // 未定義オプションはエラー
113
112
  .usage('使い方: $0 [options]')
114
113
  .option('plugin', {
115
114
  group: 'Basic Options',
116
115
  alias: 'p',
117
- choices: ["flickr","gmaps"],
116
+ choices: plugins.list(),
118
117
  demandOption: true,
119
118
  describe: '実行するプラグイン',
120
119
  type: 'string'
@@ -173,18 +172,27 @@ try {
173
172
 
174
173
  /* API Key読み込み */
175
174
  async function loadAPIKey(plugin = 'flickr') {
175
+ //ファイルチェック→環境変数チェック
176
+
176
177
  const filePath = ".API_KEY." + plugin;
177
178
  const file = resolve(filePath);
178
179
  // 存在&読取権限チェック
180
+ let key = null;
179
181
  try {
180
182
  await access(file, constants.F_OK | constants.R_OK);
183
+ // 読み込み & トリム
184
+ const raw = await readFile(file, 'utf8');
185
+ console.log(`[API KEY (${plugin}})] Read from FILE`);
186
+ key = raw.trim();
181
187
  } catch (err) {
182
- const code = /** @type {{ code?: string }} */(err).code || 'UNKNOWN';
183
- throw new Error(`APIキーのファイルにアクセスできません: ${file} (code=${code})`);
188
+ if (Object.prototype.hasOwnProperty.call(process.env, "API_KEY_" + plugin)) {
189
+ console.log(`[API KEY (${plugin}})] Read from ENV`);
190
+ key = process.env["API_KEY_" + plugin] ?? null;
191
+ } else {
192
+ const code = /** @type {{ code?: string }} */(err).code || 'UNKNOWN';
193
+ throw new Error(`APIキーのファイルもしくは環境変数にアクセスできません: ${file} (code=${code})`);
194
+ }
184
195
  }
185
- // 読み込み & トリム
186
- const raw = await readFile(file, 'utf8');
187
- const key = raw.trim();
188
196
  if (!key) {
189
197
  throw new Error(`APIキーのファイルが空です: ${file}`);
190
198
  }
@@ -372,9 +380,18 @@ try {
372
380
  }
373
381
  return [k, colors];
374
382
  }));
383
+
375
384
  // HexGrid 生成
385
+ function expandBbox(b, d, units = 'kilometers') {
386
+ //bbox拡大(指定した範囲を内包させるため)
387
+ if (!d) return b;
388
+ const poly = bboxPolygon(b);
389
+ const buff = buffer(poly, Math.sqrt(3) * d, { units }); // 周囲に d だけバッファ
390
+ return turfbbox(buff); // バッファ後の外接 bbox を返す
391
+ }
392
+ const exbbox = expandBbox(bboxArray, sizeNum, units);
393
+ const fc = hexGrid(exbbox, sizeNum, { units }).features.filter((f => booleanIntersects(f, drawn)));
376
394
 
377
- const fc = hexGrid(bboxArray, sizeNum, { units }).features.filter((f => booleanWithin(f, drawn)));
378
395
  fc.forEach((f, i) => {
379
396
  f.properties = { hexId: i + 1, triIds: [] };
380
397
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "splatone",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Multi-layer Composite Heatmap",
5
5
  "homepage": "https://github.com/YokoyamaLab/Splatone#readme",
6
6
  "bugs": {
@@ -60,7 +60,7 @@ export default async function ({
60
60
  : null;
61
61
  if (Object.keys(authors).length == 1) {
62
62
  const window = res.photos.photo[res.photos.photo.length - 1].dateupload - res.photos.photo[0].dateupload;
63
- console.warn("[Warning]", `High posting activity detected for ${Object.keys(authors)} within {$window}. the crawler will skip the next 24 hours.`);
63
+ console.warn("[Warning]", `High posting activity detected for ${Object.keys(authors)} within ${window} s. the crawler will skip the next 24 hours.`);
64
64
  next_max_upload_date -= 60 * 60 * 24;
65
65
  }
66
66
  return {
package/views/index.ejs CHANGED
@@ -11,7 +11,6 @@
11
11
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="anonymous" />
12
12
  <!-- Leaflet.draw CSS -->
13
13
  <link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" crossorigin="anonymous" />
14
-
15
14
  <link rel="stylesheet" href="/style.css" />
16
15
  </head>
17
16
 
@@ -97,6 +96,9 @@
97
96
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin="anonymous"></script>
98
97
  <!-- Leaflet.draw JS -->
99
98
  <script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js" crossorigin="anonymous"></script>
99
+ <!-- Map to Image -->
100
+ <script src="https://cdn.jsdelivr.net/npm/leaflet-easyprint@2.1.9/dist/bundle.min.js"></script>
101
+ <link href="https://cdn.jsdelivr.net/npm/leaflet-easyprint@2.1.9/libs/leaflet.min.css" rel="stylesheet">
100
102
 
101
103
  <!-- Socket.IO JS -->
102
104
  <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
@@ -196,11 +198,10 @@
196
198
  <div class="center-x">
197
199
  <button id="download-json" class="dl-btn" title="Download JSON">
198
200
  <!-- インラインSVG(ダウンロードアイコン) -->
199
- <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
200
- <path d="M12 3v9m0 0l4-4m-4 4L8 8m-5 9v2a2 2 0 002 2h14a2 2 0 002-2v-2"
201
- fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
201
+ <svg viewBox="0 0 640 640" width="16" height="16" aria-hidden="true">
202
+ <!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M128.5 64C93.2 64 64.5 92.7 64.5 128L64.5 512C64.5 547.3 93.2 576 128.5 576L384.5 576C419.8 576 448.5 547.3 448.5 512L448.5 416L526.6 416L495.6 447C486.2 456.4 486.2 471.6 495.6 480.9C505 490.2 520.2 490.3 529.5 480.9L601.5 408.9C610.9 399.5 610.9 384.3 601.5 375L529.5 303C520.1 293.6 504.9 293.6 495.6 303C486.3 312.4 486.2 327.6 495.6 336.9L526.6 367.9L448.5 367.9L448.5 234.4C448.5 217.4 441.8 201.1 429.8 189.1L323.2 82.7C311.2 70.7 295 64 278 64L128.5 64zM390 240L296.5 240C283.2 240 272.5 229.3 272.5 216L272.5 122.5L390 240zM256.5 392C256.5 378.7 267.2 368 280.5 368L384.5 368L384.5 416L280.5 416C267.2 416 256.5 405.3 256.5 392z"/>
202
203
  </svg>
203
- <span class="label">ダウンロード</span>
204
+ <span class="label">エクスポート</span>
204
205
  </button>
205
206
  </div>
206
207
  </div>`;
@@ -343,6 +344,15 @@
343
344
  });
344
345
  map.addControl(drawControl);
345
346
 
347
+ //印刷
348
+ L.easyPrint({
349
+ title: 'マップの画像化とダウンロード',
350
+ position: 'bottomright',
351
+ sizeModes: [ 'A4Landscape'],
352
+ exportOnly: true,
353
+ filename: 'splatone_export'
354
+ }).addTo(map);
355
+
346
356
  // レイヤ参照
347
357
  let hexLayer = null;
348
358
  let triLayer = null;
@@ -19,7 +19,7 @@ export default async function main(map, geojson, options = { palette: {} }) {
19
19
 
20
20
  const categoryColors = new Map();
21
21
  for (const cat in options.palette) {
22
- categoryColors.set(cat, options.palette[cat].color);
22
+ categoryColors.set(cat, options.palette[cat]);
23
23
  }
24
24
 
25
25
  const clusterByCategory = new Map(); // category -> L.MarkerClusterGroup
@@ -27,7 +27,8 @@ export default async function main(map, geojson, options = { palette: {} }) {
27
27
  function getOrCreateCluster(category) {
28
28
  if (clusterByCategory.has(category)) return clusterByCategory.get(category);
29
29
 
30
- const color = categoryColors.get(category);
30
+ const { color, darken, brighten } = categoryColors.get(category);
31
+
31
32
  // カスタム iconCreateFunction(色違いの泡)
32
33
  const group = L.markerClusterGroup({
33
34
  chunkedLoading: true,
@@ -39,9 +40,13 @@ export default async function main(map, geojson, options = { palette: {} }) {
39
40
  const count = cluster.getChildCount();
40
41
  const size = count >= 100 ? 48 : (count >= 10 ? 40 : 32);
41
42
  const html = `<div style="
42
- width:${size}px;height:${size}px;border-radius:50%;
43
- background:${color}; color:#fff; display:flex;
44
- align-items:center; justify-content:center; font-weight:700;">
43
+ width:${size}px;height:${size}px;border-radius:50%;border:3px double ${darken};
44
+ background:${color}aa; color:${brighten}; display:flex;
45
+ text-shadow: 1px 1px 0px ${darken}, -1px -1px 0px ${darken},
46
+ -1px 1px 0px ${darken}, 1px -1px 0px ${darken},
47
+ 0px 1px 0px ${darken}, 0px -1px 0px ${darken},
48
+ -1px 0px 0px ${darken}, 1px 0px 0px ${darken};
49
+ align-items:center; justify-content:center; font-weight:bold;">
45
50
  ${count}</div>`;
46
51
  return L.divIcon({
47
52
  html, className: 'marker-cluster cluster-cat', iconSize: L.point(size, size)
@@ -62,13 +67,20 @@ export default async function main(map, geojson, options = { palette: {} }) {
62
67
 
63
68
  // マーカーを作るユーティリティ
64
69
  const addMarker = (lng, lat) => {
65
- const m = L.marker([lat, lng], {
66
- // 見た目を点寄りにしたい場合は小さな divIcon にしてもOK
67
- // icon: L.divIcon({className:'', html:'<div style="width:8px;height:8px;border-radius:50%;background:#444"></div>', iconSize:[8,8]})
68
- });
69
70
  const props = feature.properties ?? {};
70
71
  const name = props.name ?? '(no name)';
71
72
  const cat = props.category ?? 'uncategorized';
73
+ const { color, darken, brighten } = categoryColors.get(cat);
74
+ const m = L.marker([lat, lng], {
75
+ // 見た目を点寄りにしたい場合は小さな divIcon にしてもOK
76
+ icon: L.divIcon({
77
+ className: '', html: `<div style="
78
+ width:8px;height:8px;border-radius:50%;
79
+ background:${color}aa;
80
+ border:1px solid ${darken};"
81
+ ></div>`, iconSize: [8, 8]
82
+ })
83
+ });
72
84
  m.bindPopup(`<b>${name}</b><br/>category: ${cat}<br/>${Number(lat).toFixed(5)}, ${Number(lng).toFixed(5)}`);
73
85
  markers.push(m);
74
86
  };
@@ -102,7 +114,7 @@ export default async function main(map, geojson, options = { palette: {} }) {
102
114
  const bounds = L.latLngBounds();
103
115
 
104
116
  for (const f of featureCollection.features ?? []) {
105
- const category = f.properties.category;
117
+ const category = f.properties.category;
106
118
  const cluster = getOrCreateCluster(category);
107
119
  const markers = featureToMarkers(f);
108
120
  if (markers.length) {