splatone 0.0.1
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/.gitattributes +2 -0
- package/.vscode/settings.json +1 -0
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/crawler.js +570 -0
- package/lib/PluginBase.js +23 -0
- package/lib/VisualizerBase.js +15 -0
- package/lib/paletteGenerator.js +532 -0
- package/lib/pluginLoader.js +146 -0
- package/lib/util.js +180 -0
- package/package.json +36 -0
- package/plugins/flickr/index.js +55 -0
- package/plugins/flickr/worker.js +77 -0
- package/public/style.css +318 -0
- package/public/visualizer.js +49 -0
- package/views/index.ejs +740 -0
- package/visualizer/bulky/node.js +41 -0
- package/visualizer/bulky/web.js +29 -0
- package/visualizer/marker-cluster/node.js +50 -0
- package/visualizer/marker-cluster/public/style.css +1 -0
- package/visualizer/marker-cluster/web.js +153 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { VisualizerBase } from '../../lib/VisualizerBase.js';
|
|
2
|
+
import { featureCollection } from "@turf/turf";
|
|
3
|
+
import { writeFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
export default class BulkyVisualizer extends VisualizerBase {
|
|
6
|
+
static id = 'bulky'; // 一意ID(フォルダ名と一致させると運用しやすい)
|
|
7
|
+
static name = 'Bulky Visualizer'; // 表示名
|
|
8
|
+
static version = '0.0.0';
|
|
9
|
+
static description = "全データをCircleMarkerとして地図上に表示";
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getFutureCollection(result, target){
|
|
16
|
+
//console.log(JSON.stringify(target, null, 4));
|
|
17
|
+
const layers = {};
|
|
18
|
+
for (const hex in result) {
|
|
19
|
+
for (const cat in result[hex]) {
|
|
20
|
+
if (!layers.hasOwnProperty(cat)) {
|
|
21
|
+
layers[cat] = [];
|
|
22
|
+
}
|
|
23
|
+
for (const feature of result[hex][cat].items.features) {
|
|
24
|
+
feature.properties["radius"] = 5;
|
|
25
|
+
|
|
26
|
+
feature.properties["stroke"] = true;
|
|
27
|
+
feature.properties["color"] = target.splatonePalette[cat].darken;
|
|
28
|
+
feature.properties["weight"] = 1;
|
|
29
|
+
feature.properties["opacity"] = 1;
|
|
30
|
+
|
|
31
|
+
feature.properties["fill"] = true;
|
|
32
|
+
feature.properties["fillColor"] = target.splatonePalette[cat].color;
|
|
33
|
+
feature.properties["fillOpacity"] = .5;
|
|
34
|
+
|
|
35
|
+
layers[cat].push(feature);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return Object.fromEntries(Object.entries(layers).map(([k, v]) => [k, featureCollection(v)]));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
let booted = false;
|
|
2
|
+
export default async function main(map, geojson, options = {}) {
|
|
3
|
+
if (booted) return;
|
|
4
|
+
booted = true;
|
|
5
|
+
const layers = {};
|
|
6
|
+
for (const cat in geojson) {
|
|
7
|
+
const layer = addGeoJSONLayer(map, geojson[cat], {
|
|
8
|
+
pointToLayer: (feature, latlng) => {
|
|
9
|
+
return L.circleMarker(latlng, {
|
|
10
|
+
radius: feature.properties.radius,
|
|
11
|
+
stroke: feature.properties.stroke,
|
|
12
|
+
color: feature.properties.color,
|
|
13
|
+
weight: feature.properties.weight,
|
|
14
|
+
opacity: feature.properties.opacity,
|
|
15
|
+
fill: feature.properties.fill,
|
|
16
|
+
fillColor: feature.properties.fillColor,
|
|
17
|
+
fillOpacity: feature.properties.fillOpacity
|
|
18
|
+
})
|
|
19
|
+
},
|
|
20
|
+
onEachFeature: (feature, layer) => {
|
|
21
|
+
const t = `<img src="${feature.properties.url_s}">` || "NO Image!";
|
|
22
|
+
layer.bindTooltip(t ?? '', { direction: 'top', opacity: 0.9 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
});
|
|
26
|
+
layers[cat] = layer;
|
|
27
|
+
}
|
|
28
|
+
return layers;
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { VisualizerBase } from '../../lib/VisualizerBase.js';
|
|
2
|
+
import { featureCollection } from "@turf/turf";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export default class MarkerClusterVisualizer extends VisualizerBase {
|
|
6
|
+
static id = 'marker-cluster'; // 一意ID(フォルダ名と一致させると運用しやすい)
|
|
7
|
+
static name = 'Marker Cluster Visualizer'; // 表示名
|
|
8
|
+
static version = '0.0.0';
|
|
9
|
+
static description = "マーカークラスターとして地図上に表示";
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
concatFC(fcA, fcB) {
|
|
16
|
+
return {
|
|
17
|
+
type: "FeatureCollection",
|
|
18
|
+
features: [
|
|
19
|
+
...(fcA?.features ?? []),
|
|
20
|
+
...(fcB?.features ?? []),
|
|
21
|
+
]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
addCategory(fc, value, key = "category", overwrite = true) {
|
|
25
|
+
const features = (fc?.features ?? []).map(f => {
|
|
26
|
+
const props = { ...(f.properties ?? {}) };
|
|
27
|
+
if (overwrite || props[key] == null) {
|
|
28
|
+
props[key] = value;
|
|
29
|
+
}
|
|
30
|
+
return { ...f, properties: props };
|
|
31
|
+
});
|
|
32
|
+
return { type: "FeatureCollection", features };
|
|
33
|
+
}
|
|
34
|
+
getFutureCollection(result, target) {
|
|
35
|
+
//console.log(JSON.stringify(target, null, 4));
|
|
36
|
+
const layers = {};
|
|
37
|
+
for (const hex in result) {
|
|
38
|
+
for (const cat in result[hex]) {
|
|
39
|
+
if (!layers.hasOwnProperty(cat)) {
|
|
40
|
+
layers[cat] = [];
|
|
41
|
+
}
|
|
42
|
+
for (const feature of result[hex][cat].items.features) {
|
|
43
|
+
feature.properties["category"] = cat;
|
|
44
|
+
layers[cat].push(feature);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return Object.fromEntries(Object.entries(layers).map(([k, v]) => [k, featureCollection(v)]));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.cluster-cat .marker-cluster div { color: #fff; font-weight: 700; }
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
let booted = false;
|
|
2
|
+
export default async function main(map, geojson, options = { palette: {} }) {
|
|
3
|
+
if (booted) return;
|
|
4
|
+
booted = true;
|
|
5
|
+
|
|
6
|
+
const urls = [
|
|
7
|
+
'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css',
|
|
8
|
+
'https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.Default.css',
|
|
9
|
+
'https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js',
|
|
10
|
+
'./visualizer/marker-cluster/public/style.css',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await Promise.all(urls.map(loadAsset));
|
|
15
|
+
console.log('すべて読み込み完了!');
|
|
16
|
+
} catch (err) {
|
|
17
|
+
console.error('どれかの読み込みに失敗:', err);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const categoryColors = new Map();
|
|
21
|
+
for (const cat in options.palette) {
|
|
22
|
+
categoryColors.set(cat, options.palette[cat].color);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const clusterByCategory = new Map(); // category -> L.MarkerClusterGroup
|
|
26
|
+
const overlaysForControl = {};
|
|
27
|
+
function getOrCreateCluster(category) {
|
|
28
|
+
if (clusterByCategory.has(category)) return clusterByCategory.get(category);
|
|
29
|
+
|
|
30
|
+
const color = categoryColors.get(category);
|
|
31
|
+
// カスタム iconCreateFunction(色違いの泡)
|
|
32
|
+
const group = L.markerClusterGroup({
|
|
33
|
+
chunkedLoading: true,
|
|
34
|
+
disableClusteringAtZoom: 18,
|
|
35
|
+
maxClusterRadius: 60,
|
|
36
|
+
spiderfyOnMaxZoom: true,
|
|
37
|
+
showCoverageOnHover: false,
|
|
38
|
+
iconCreateFunction: (cluster) => {
|
|
39
|
+
const count = cluster.getChildCount();
|
|
40
|
+
const size = count >= 100 ? 48 : (count >= 10 ? 40 : 32);
|
|
41
|
+
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;">
|
|
45
|
+
${count}</div>`;
|
|
46
|
+
return L.divIcon({
|
|
47
|
+
html, className: 'marker-cluster cluster-cat', iconSize: L.point(size, size)
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
clusterByCategory.set(category, group);
|
|
53
|
+
// レイヤコントロールに追加(カテゴリ名をラベルに)
|
|
54
|
+
overlaysForControl[`MarkerCluster: ${category}`] = group;
|
|
55
|
+
return group;
|
|
56
|
+
}
|
|
57
|
+
// ====== Feature をマーカー(代表点)に変換 ======
|
|
58
|
+
function featureToMarkers(feature) {
|
|
59
|
+
const g = feature.geometry;
|
|
60
|
+
const markers = [];
|
|
61
|
+
if (!g) return markers;
|
|
62
|
+
|
|
63
|
+
// マーカーを作るユーティリティ
|
|
64
|
+
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
|
+
const props = feature.properties ?? {};
|
|
70
|
+
const name = props.name ?? '(no name)';
|
|
71
|
+
const cat = props.category ?? 'uncategorized';
|
|
72
|
+
m.bindPopup(`<b>${name}</b><br/>category: ${cat}<br/>${Number(lat).toFixed(5)}, ${Number(lng).toFixed(5)}`);
|
|
73
|
+
markers.push(m);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
switch (g.type) {
|
|
77
|
+
case 'Point': {
|
|
78
|
+
const [lng, lat] = g.coordinates;
|
|
79
|
+
addMarker(lng, lat);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'MultiPoint': {
|
|
83
|
+
for (const [lng, lat] of g.coordinates) addMarker(lng, lat);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
default: {
|
|
87
|
+
// Polygon/LineString 系は代表点化(重心が外へ出ることがあるため pointOnFeature を採用)
|
|
88
|
+
try {
|
|
89
|
+
const pof = turf.pointOnFeature(feature);
|
|
90
|
+
const [lng, lat] = pof.geometry.coordinates;
|
|
91
|
+
addMarker(lng, lat);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.warn('pointOnFeature failed:', e, feature);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return markers;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ====== 追加処理:カテゴリ別にクラスタへ投入 ======
|
|
101
|
+
function addFeatureCollectionByCategory(featureCollection, categoryKey = 'category') {
|
|
102
|
+
const bounds = L.latLngBounds();
|
|
103
|
+
|
|
104
|
+
for (const f of featureCollection.features ?? []) {
|
|
105
|
+
const category = f.properties.category;
|
|
106
|
+
const cluster = getOrCreateCluster(category);
|
|
107
|
+
const markers = featureToMarkers(f);
|
|
108
|
+
if (markers.length) {
|
|
109
|
+
cluster.addLayers(markers);
|
|
110
|
+
for (const m of markers) bounds.extend(m.getLatLng());
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// まだマップに載っていないクラスターは載せる
|
|
115
|
+
for (const [cat, grp] of clusterByCategory.entries()) {
|
|
116
|
+
console.log("add map", cat);
|
|
117
|
+
if (!map.hasLayer(grp)) grp.addTo(map);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
if (!layerControl) {
|
|
122
|
+
layerControl = L.control.layers({}, overlaysForControl, { collapsed: false }).addTo(map);
|
|
123
|
+
} else {
|
|
124
|
+
// 既存コントロールにも反映(念のため)
|
|
125
|
+
layerControl.remove();
|
|
126
|
+
layerControl = L.control.layers({}, overlaysForControl, { collapsed: false }).addTo(map);
|
|
127
|
+
}
|
|
128
|
+
*/
|
|
129
|
+
//if (bounds.isValid()) map.fitBounds(bounds.pad(0.1));
|
|
130
|
+
//renderLegend();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ====== 凡例 ======
|
|
134
|
+
function renderLegend() {
|
|
135
|
+
const el = document.getElementById('map_legend');
|
|
136
|
+
el.innerHTML = '<div style="font-weight:700;margin-bottom:4px;">Categories</div>';
|
|
137
|
+
for (const [cat, color] of categoryColors.entries()) {
|
|
138
|
+
const item = document.createElement('div');
|
|
139
|
+
item.className = 'legend-item';
|
|
140
|
+
item.innerHTML = `<span class="legend-swatch" style="background:${color}"></span><span>${cat}</span>`;
|
|
141
|
+
el.appendChild(item);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let layerControl = null;
|
|
145
|
+
|
|
146
|
+
// 実データを投入(fetchで置き換え可)
|
|
147
|
+
for (const cat in geojson) {
|
|
148
|
+
console.log(cat);
|
|
149
|
+
addFeatureCollectionByCategory(geojson[cat], cat);
|
|
150
|
+
}
|
|
151
|
+
// Mapから単なるObjectへ
|
|
152
|
+
return Object.fromEntries(clusterByCategory);
|
|
153
|
+
}
|