Files
next-tjwater-frontend/src/features/workbench/map/sources.ts
T
jiang 31acd33a2e fix: probe all non-core GeoServer layers
Extend availability checks beyond pumps and tanks so unpublished valve and reservoir layers cannot leave stale controls.
2026-09-11 17:00:54 +08:00

237 lines
6.4 KiB
TypeScript

import type { StyleSpecification, VectorSourceSpecification } from "maplibre-gl";
import { GEOSERVER_WORKSPACE, MAP_URL } from "./geoserver-config";
import { MAP_STYLE_TOKENS } from "./map-colors";
import {
SCADA_ANALYSIS_SOURCE_ID,
createEmptyScadaAnalysisCollection
} from "./scada-analysis";
import { SCADA_SOURCE_ID } from "./scada";
export const WATER_NETWORK_GLOBAL_VIEW = {
bbox3857: [13508801.930066336, 3608163.3499832638, 13555650.638648884, 3633685.137885742],
bbox4326: [121.35162408429075, 30.81049932304578, 121.77248479490075, 31.007207809222017]
} as const;
export const SOURCE_LAYERS = {
pipes: "pipes",
junctions: "junctions",
valves: "valves",
reservoirs: "reservoirs",
pumps: "pumps",
tanks: "tanks"
} as const;
export const WATER_NETWORK_SOURCE_IDS = [
"pipes",
"junctions",
"valves",
"reservoirs",
"scada",
"pumps",
"tanks"
] as const;
export type WaterNetworkSourceId = (typeof WATER_NETWORK_SOURCE_IDS)[number];
export type SupplyLayerCatalogItem = {
id: WaterNetworkSourceId;
sourceLayer?: (typeof SOURCE_LAYERS)[keyof typeof SOURCE_LAYERS];
geometry: "line" | "point";
available: boolean;
availability: "required" | "probe";
label: string;
icon: string;
};
export const SUPPLY_LAYER_CATALOG = [
{
id: "pipes",
sourceLayer: SOURCE_LAYERS.pipes,
geometry: "line",
available: true,
availability: "required",
label: "管线",
icon: "pipe"
},
{
id: "junctions",
sourceLayer: SOURCE_LAYERS.junctions,
geometry: "point",
available: true,
availability: "required",
label: "节点",
icon: "junction"
},
{
id: "valves",
sourceLayer: SOURCE_LAYERS.valves,
geometry: "point",
available: true,
availability: "probe",
label: "阀门",
icon: "valve"
},
{
id: "reservoirs",
sourceLayer: SOURCE_LAYERS.reservoirs,
geometry: "point",
available: true,
availability: "probe",
label: "水库",
icon: "reservoir"
},
{
id: "scada",
sourceLayer: undefined,
geometry: "point",
available: true,
availability: "required",
label: "SCADA",
icon: "scada-pressure"
},
{
id: "pumps",
sourceLayer: SOURCE_LAYERS.pumps,
geometry: "point",
available: true,
availability: "probe",
label: "水泵",
icon: "pump"
},
{
id: "tanks",
sourceLayer: SOURCE_LAYERS.tanks,
geometry: "point",
available: true,
availability: "probe",
label: "水箱",
icon: "tank"
}
] as const satisfies readonly SupplyLayerCatalogItem[];
export const AVAILABLE_WATER_NETWORK_SOURCE_IDS = SUPPLY_LAYER_CATALOG.filter(
(layer) => layer.available
).map((layer) => layer.id) as WaterNetworkSourceId[];
export type AvailableWaterNetworkSourceId = (typeof AVAILABLE_WATER_NETWORK_SOURCE_IDS)[number];
export type GeoServerWaterNetworkSourceId = Exclude<AvailableWaterNetworkSourceId, "scada">;
export const GEOSERVER_WATER_NETWORK_SOURCE_IDS = AVAILABLE_WATER_NETWORK_SOURCE_IDS.filter(
(sourceId): sourceId is GeoServerWaterNetworkSourceId => sourceId !== "scada"
);
export function isAvailableWaterNetworkSourceId(
value: string
): value is AvailableWaterNetworkSourceId {
return (AVAILABLE_WATER_NETWORK_SOURCE_IDS as readonly string[]).includes(value);
}
export function createWaterNetworkSources(): Record<GeoServerWaterNetworkSourceId, VectorSourceSpecification>;
export function createWaterNetworkSources(
sourceIds: readonly GeoServerWaterNetworkSourceId[]
): Partial<Record<GeoServerWaterNetworkSourceId, VectorSourceSpecification>>;
export function createWaterNetworkSources(
sourceIds: readonly GeoServerWaterNetworkSourceId[] = GEOSERVER_WATER_NETWORK_SOURCE_IDS
) {
return Object.fromEntries(
SUPPLY_LAYER_CATALOG.flatMap((layer) =>
layer.available &&
layer.sourceLayer !== undefined &&
sourceIds.includes(layer.id as GeoServerWaterNetworkSourceId)
? [[layer.id, createGeoServerVectorSource(layer.sourceLayer)] as const]
: []
)
) as Partial<Record<GeoServerWaterNetworkSourceId, VectorSourceSpecification>>;
}
function createGeoServerVectorSource(
sourceLayer: (typeof SOURCE_LAYERS)[keyof typeof SOURCE_LAYERS]
): VectorSourceSpecification {
return {
type: "vector",
promoteId: "id",
tiles: [
`${MAP_URL}/gwc/service/wmts/rest/${GEOSERVER_WORKSPACE}:${sourceLayer}/WebMercatorQuad/{z}/{y}/{x}?format=application/vnd.mapbox-vector-tile`
],
minzoom: 0,
maxzoom: 24,
bounds: [...WATER_NETWORK_GLOBAL_VIEW.bbox4326]
};
}
export function createBaseStyle(mapboxToken?: string): StyleSpecification {
const sources: StyleSpecification["sources"] = {
[SCADA_SOURCE_ID]: {
type: "geojson",
promoteId: "device_id",
data: { type: "FeatureCollection", features: [] }
},
[SCADA_ANALYSIS_SOURCE_ID]: {
type: "geojson",
data: createEmptyScadaAnalysisCollection()
}
};
const layers: StyleSpecification["layers"] = [
{
id: "background",
type: "background",
paint: {
"background-color": MAP_STYLE_TOKENS.canvas.primary
}
}
];
if (mapboxToken) {
sources["mapbox-light"] = {
type: "raster",
tiles: [
`https://api.mapbox.com/styles/v1/mapbox/light-v11/tiles/512/{z}/{x}/{y}@2x?access_token=${mapboxToken}`
],
tileSize: 512,
attribution:
'© <a href="https://www.mapbox.com/about/maps/">Mapbox</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
};
sources["mapbox-satellite"] = {
type: "raster",
tiles: [
`https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/tiles/512/{z}/{x}/{y}@2x?access_token=${mapboxToken}`
],
tileSize: 512,
attribution:
'© <a href="https://www.mapbox.com/about/maps/">Mapbox</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
};
layers.push({
id: "mapbox-light",
type: "raster",
source: "mapbox-light",
paint: {
"raster-opacity": 0.82,
"raster-saturation": -0.22,
"raster-contrast": 0.04
}
});
layers.push({
id: "mapbox-satellite",
type: "raster",
source: "mapbox-satellite",
layout: {
visibility: "none"
},
paint: {
"raster-opacity": 0.86,
"raster-saturation": -0.3,
"raster-contrast": 0.1
}
});
}
return {
version: 8,
glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
sources,
layers
};
}