-
Notifications
You must be signed in to change notification settings - Fork 7
Module SimpleStyle
embed は SimpleStyle 仕様 (mapbox の GeoJSON style 規約) を実装した 2 クラスを提供する:
-
SimpleStyle(src/lib/simplestyle.ts, 435 行) — GeoJSON 入力 -
SimpleStyleVector(src/lib/simplestyle-vector.ts, 257 行) — Vector Tile 入力
両者はほぼ同じレイヤ構造を持つが、ソース (geojson vs vector) と一部の機能 (cluster は GeoJSON のみ) が異なる。
export class SimpleStyle {
public _loadingPromise: Promise<unknown>;
private callFitBounds: boolean = false;
private geojson: any;
private map: maplibregl.Map;
private options: {
id: string;
cluster: boolean;
heatmap: boolean;
clusterColor: string;
[key: string]: any;
};
private _eventHandlers: Array<{ event: string; layer: string; handler: Function }>;
constructor(geojson: string | GeoJSON, options?: Partial<typeof this.options>);
setGeoJSON(geojson: string | GeoJSON): void;
updateData(geojson: GeoJSON): this;
addTo(map: maplibregl.Map): this;
fitBounds(options?: maplibregl.FitBoundsOptions): this;
remove(): this;
// private helpers
setPolygonGeometries(): void;
setLineGeometries(): void;
setPointGeometries(): void;
setPopup(map, source): Promise<void>;
setCluster(): void;
}constructor(geojson, options?) {
this.setGeoJSON(geojson);
this.options = {
id: 'geolonia-simple-style',
cluster: true,
heatmap: false, // TODO: It should support heatmap.
clusterColor: '#ff0000',
...options,
};
}-
setGeoJSON(geojson)を呼び出し - 既定 options は
id,cluster,heatmap,clusterColor - options は spread で上書き
setGeoJSON(geojson) {
if (typeof geojson === 'string' && isURL(geojson)) {
this.geojson = template; // 空の FeatureCollection
const fetchGeoJSON = async () => {
try {
const response = await window.fetch(geojson);
const data = response.ok ? await response.json() : {};
this.geojson = data;
this.updateData(data);
if (this.callFitBounds) {
this.fitBounds();
this.callFitBounds = false;
}
} catch (error) {
console.error('[Geolonia] Failed to load GeoJSON:', error);
}
};
this._loadingPromise = fetchGeoJSON();
} else {
this.geojson = geojson;
}
}- 文字列で URL ならば fetch (非同期)、template (空 FeatureCollection) を仮データとして使用
- 取得完了後
updateDataで source を更新 - それ以外 (オブジェクト) はそのまま設定
template (simplestyle.ts:13-16):
const template = {
type: 'FeatureCollection',
features: [],
};this.map = map- features を polygon/linestring と point に振り分け
- source
${id}(polygon/linestring 用、cluster なし) を追加 -
setPolygonGeometries()で fill レイヤ + popup -
setLineGeometries()で line レイヤ + popup - source
${id}-points(point + cluster) を追加 (clusterMaxZoom: 14,clusterRadius: 50) - polygon-symbol レイヤ追加 (テキストラベル)
- linestring-symbol レイヤ追加 (line に沿ったテキスト)
-
setPointGeometries()で circle / symbol レイヤ + popup -
setCluster()で cluster 円 + count + cluster click handler
| ID | type | source | filter | 役割 |
|---|---|---|---|---|
${id}-polygon |
fill | ${id} |
$type == Polygon |
ポリゴン塗り |
${id}-linestring |
line | ${id} |
$type == LineString |
線 |
${id}-polygon-symbol |
symbol | ${id} |
$type == Polygon |
ポリゴンラベル |
${id}-linestring-symbol |
symbol | ${id} |
$type == LineString |
線ラベル |
${id}-circle-points |
circle | ${id}-points |
!point_count && !marker-symbol |
ポイント円 |
${id}-symbol-points |
symbol | ${id}-points |
!point_count |
ポイントアイコン + ラベル |
${id}-clusters |
circle | ${id}-points |
has point_count |
クラスタ円 |
${id}-cluster-count |
symbol | ${id}-points |
has point_count |
クラスタ数表示 |
this.map.addLayer({
id: `${this.options.id}-clusters`,
type: 'circle',
source: `${this.options.id}-points`,
filter: ['has', 'point_count'],
paint: {
'circle-radius': 20,
'circle-color': this.options.clusterColor,
'circle-opacity': 1.0,
},
});
this.map.addLayer({
id: `${this.options.id}-cluster-count`,
type: 'symbol',
source: `${this.options.id}-points`,
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-size': 14,
'text-font': ['Noto Sans Regular'],
},
});Cluster click handler (simplestyle.ts:341-354) で queryRenderedFeatures → getClusterExpansionZoom → easeTo。
async setPopup(map, source) {
const clickHandler = async (e) => {
const center = turfCenter(e.features[0]).geometry.coordinates;
const description = e.features[0].properties.description;
if (description) {
const sanitizedDescription = await sanitizeDescription(description);
new maplibregl.Popup()
.setLngLat(center)
.setHTML(sanitizedDescription)
.addTo(map);
}
};
const mouseEnterHandler = (e) => {
if (e.features[0].properties.description) {
map.getCanvas().style.cursor = 'pointer';
}
};
const mouseLeaveHandler = () => {
map.getCanvas().style.cursor = '';
};
map.on('click', source, clickHandler);
map.on('mouseenter', source, mouseEnterHandler);
map.on('mouseleave', source, mouseLeaveHandler);
this._eventHandlers.push(
{ event: 'click', layer: source, handler: clickHandler },
{ event: 'mouseenter', layer: source, handler: mouseEnterHandler },
{ event: 'mouseleave', layer: source, handler: mouseLeaveHandler },
);
}- features[0] の中心 (turfCenter) を popup 位置に
- description プロパティを sanitize して HTML に
- カーソル変更
-
_eventHandlers配列に登録し、remove()時に off できる
remove() {
if (!this.map) return this;
const id = this.options.id;
for (const { event, layer, handler } of this._eventHandlers) {
this.map.off(event, layer, handler);
}
this._eventHandlers = [];
const layerIds = [
`${id}-polygon-symbol`,
`${id}-linestring-symbol`,
`${id}-circle-points`,
`${id}-symbol-points`,
`${id}-polygon`,
`${id}-linestring`,
`${id}-clusters`,
`${id}-cluster-count`,
];
for (const layerId of layerIds) {
if (this.map.getLayer(layerId)) {
this.map.removeLayer(layerId);
}
}
for (const sourceId of [id, `${id}-points`]) {
if (this.map.getSource(sourceId)) {
this.map.removeSource(sourceId);
}
}
this.map.getCanvas().style.cursor = '';
return this;
}完全クリーンアップ:
- イベントハンドラ off
- 全レイヤ削除
- ソース削除
- カーソルリセット
fitBounds(options = {}) {
this.callFitBounds = true;
const _options = {
duration: 3000,
padding: 30,
...options,
};
const bounds = geojsonExtent(this.geojson);
if (bounds) {
window.requestAnimationFrame(() => {
this.map.fitBounds(bounds, _options);
});
}
return this;
}-
@mapbox/geojson-extentで bbox 計算 -
requestAnimationFrame内でmap.fitBounds呼び出し -
this.callFitBounds = trueを立てるのは、URL fetch 完了後のsetGeoJSON側で再度 fitBounds を呼ぶための fallback (simplestyle.ts:421-424)
updateData(geojson) {
this.setGeoJSON(geojson);
const features = this.geojson.features;
const polygonAndLines = features.filter(...);
const points = features.filter(...);
this.map.getSource(this.options.id).setData({...});
this.map.getSource(`${this.options.id}-points`).setData({...});
return this;
}source の setData で既存レイヤを再利用しつつデータ更新。
class SimpleStyleVector {
private sourceName: string = 'vt-geolonia-simple-style';
constructor(private url: string);
addTo(map: maplibregl.Map): void;
// private
setPolygonGeometries(map): void;
setLineGeometries(map): void;
setPointGeometries(map): void;
setPopup(map, source): Promise<void>;
}
export default SimpleStyleVector;constructor(private url) {
this.sourceName = 'vt-geolonia-simple-style';
}url は parseSimpleVector を通った後の値 (geolonia://tiles/custom/{id} または http(s)://...)。addSource 時に maplibre が TileJSON エンドポイントとして使用する。
-
data-lng/data-latが両方未指定の時のみsourcedataイベントでfitBounds(source.bounds, { duration: 0, padding: 30 })を 1 回実行 (initialZoomDoneフラグで再入防止) (simplestyle-vector.ts:22-53) -
addSource('vt-geolonia-simple-style', { type: 'vector', url: this.url })(simplestyle-vector.ts:55-58) - setPolygonGeometries, setLineGeometries (
simplestyle-vector.ts:60-61) - polygon-symbol レイヤ追加
- linestring-symbol レイヤ追加
- setPointGeometries
全レイヤの source-layer が 'g-simplestyle-v1' 固定 (simplestyle-vector.ts:67,91,125,147,173,195)。サーバ側のベクトルタイル仕様に依存。
GeoJSON 版と微妙に異なる:
| ID | type | source | filter |
|---|---|---|---|
vt-geolonia-simple-style-polygon |
fill | sourceName | $type == Polygon |
vt-geolonia-simple-style-linestring |
line | sourceName | $type == LineString |
vt-geolonia-simple-style-polygon-symbol |
symbol | sourceName | $type == Polygon |
vt-geolonia-simple-style-linestring-symbol |
symbol | sourceName | $type == LineString |
vt-circle-simple-style-points |
circle | sourceName | $type == Point && !marker-symbol |
vt-geolonia-simple-style-points |
symbol | sourceName | $type == Point |
vt-circle-simple-style-points のみ prefix が vt-circle- で他と非対称。なぜ統一されていないかは不明 (Suspicious-Logic 参照)。
SimpleStyleVector には remove() が実装されていない。addTo 内で map.on('sourcedata', ...) で listener を永続登録するため、マップが remove されても listener はリーク。
GeoJSON 版とは対照的 (こちらは remove() あり)。
GeoJSON 版と同じ logic だが、_eventHandlers 配列に push しない (simplestyle-vector.ts:230-254)。remove が無いので意味が無い → 無いまま、という設計。
両クラスとも以下の properties を読む (詳細は Hidden-Spec):
-
marker-color,marker-size('small'/'large'/その他),marker-symbol -
fill,fill-opacity -
stroke,stroke-width,stroke-opacity -
text-color,text-halo-color -
title,description
simplestyle.ts:8-11 と simplestyle-vector.ts:7-10 で同じ定数が別々に定義されている (DRY 違反):
const textColor = '#000000';
const textHaloColor = '#FFFFFF';
const backgroundColor = 'rgba(255, 0, 0, 0.4)';
const strokeColor = '#FFFFFF';書き直し時には共通モジュールに集約可能。
-
SimpleStyleVectorのremove()欠落 - レイヤ ID の prefix 非対称 (
vt-circle-vsvt-geolonia-) -
geolonia-simple-styleID prefix で複数インスタンス衝突 -
SimpleStyle.heatmapが TODO - 色定数の二重定義
- maps-core (lib) 側に置く
- 共通レイヤ生成ロジックを base class または mixin にまとめる
-
SimpleStyleVectorの lifecycle 管理 (remove) を追加 - id を constructor 引数で受け取り、複数インスタンス共存を保証
- 色定数を共通モジュールへ
- heatmap を実装するか TODO を削除するか判断