UNPKG

protomaps-leaflet

Version:

Vector tile rendering and labeling for [Leaflet](https://github.com/Leaflet/Leaflet).

658 lines (652 loc) 16.5 kB
import { type Flavor } from "@protomaps/basemaps"; import { mix } from "color2k"; import { LabelRule } from "../labeler"; import { PaintRule } from "../painter"; import { CenteredTextSymbolizer, CircleSymbolizer, GroupSymbolizer, LineLabelSymbolizer, LineSymbolizer, OffsetTextSymbolizer, PolygonSymbolizer, exp, linear, } from "../symbolizer"; import { Feature, GeomType, JsonObject } from "../tilecache"; const getString = (props: JsonObject, key: string): string => { const val = props[key]; if (typeof val === "string") return val; return ""; }; const getNumber = (props: JsonObject, key: string): number => { const val = props[key]; if (typeof val === "number") return val; return 0; }; export const paintRules = (t: Flavor): PaintRule[] => { return [ { dataLayer: "earth", symbolizer: new PolygonSymbolizer({ fill: t.earth, }), }, ...(t.landcover ? [ { dataLayer: "landcover", symbolizer: new PolygonSymbolizer({ fill: (z, f) => { const landcover = t.landcover; if (!landcover || !f) return ""; const kind = getString(f.props, "kind"); if (kind === "grassland") return landcover.grassland; if (kind === "barren") return landcover.barren; if (kind === "urban_area") return landcover.urban_area; if (kind === "farmland") return landcover.farmland; if (kind === "glacier") return landcover.glacier; if (kind === "scrub") return landcover.scrub; return landcover.forest; }, opacity: (z, f) => { if (z === 8) return 0.5; return 1; }, }), }, ] : []), { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: (z, f) => { return mix(t.park_a, t.park_b, Math.min(Math.max(z / 12.0, 12), 0)); }, }), filter: (z, f) => { const kind = getString(f.props, "kind"); return ["allotments", "village_green", "playground"].includes(kind); }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.park_b, opacity: (z, f) => { if (z < 8) return 0; if (z === 8) return 0.5; return 1; }, }), filter: (z, f) => { const kind = getString(f.props, "kind"); return [ "national_park", "park", "cemetery", "protected_area", "nature_reserve", "forest", "golf_course", ].includes(kind); }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.hospital, }), filter: (z, f) => { return f.props.kind === "hospital"; }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.industrial, }), filter: (z, f) => { return f.props.kind === "industrial"; }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.school, }), filter: (z, f) => { const kind = getString(f.props, "kind"); return ["school", "university", "college"].includes(kind); }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.beach, }), filter: (z, f) => { return f.props.kind === "beach"; }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.zoo, }), filter: (z, f) => { return f.props.kind === "zoo"; }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.zoo, }), filter: (z, f) => { const kind = getString(f.props, "kind"); return ["military", "naval_base", "airfield"].includes(kind); }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: (z, f) => { return mix(t.wood_a, t.wood_b, Math.min(Math.max(z / 12.0, 12), 0)); }, opacity: (z, f) => { if (z < 8) return 0; if (z === 8) return 0.5; return 1; }, }), filter: (z, f) => { const kind = getString(f.props, "kind"); return ["wood", "nature_reserve", "forest"].includes(kind); }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: (z, f) => { return mix(t.scrub_a, t.scrub_b, Math.min(Math.max(z / 12.0, 12), 0)); }, }), filter: (z, f) => { const kind = getString(f.props, "kind"); return ["scrub", "grassland", "grass"].includes(kind); }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.scrub_b, }), filter: (z, f) => { const kind = getString(f.props, "kind"); return ["scrub", "grassland", "grass"].includes(kind); }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.glacier, }), filter: (z, f) => { return f.props.kind === "glacier"; }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.sand, opacity: (z, f) => { if (z < 8) return 0; if (z === 8) return 0.5; return 1; }, }), filter: (z, f) => { return f.props.kind === "sand"; }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.aerodrome, }), filter: (z, f) => { return f.props.kind === "aerodrome"; }, }, { dataLayer: "water", symbolizer: new PolygonSymbolizer({ fill: t.water, }), filter: (z, f) => { return f.geomType === GeomType.Polygon; }, }, { dataLayer: "roads", symbolizer: new LineSymbolizer({ color: t.runway, width: (z, f) => { return exp(1.6, [ [11, 0], [13, 4], [19, 30], ])(z); }, }), filter: (z, f) => { return f.props.kind_detail === "runway"; }, }, { dataLayer: "roads", symbolizer: new LineSymbolizer({ color: t.runway, width: (z, f) => { return exp(1.6, [ [14, 0], [14.5, 1], [16, 6], ])(z); }, }), filter: (z, f) => { return f.props.kind_detail === "taxiway"; }, }, { dataLayer: "roads", symbolizer: new LineSymbolizer({ color: t.pier, width: (z, f) => { return exp(1.6, [ [13, 0], [13.5, 0, 5], [21, 16], ])(z); }, }), filter: (z, f) => { return f.props.kind === "path" && f.props.kind_detail === "pier"; }, }, { dataLayer: "water", minzoom: 14, symbolizer: new LineSymbolizer({ color: t.water, width: (z, f) => { return exp(1.6, [ [9, 0], [9.5, 1.0], [18, 12], ])(z); }, }), filter: (z, f) => { return f.geomType === GeomType.Line && f.props.kind === "river"; }, }, { dataLayer: "water", minzoom: 14, symbolizer: new LineSymbolizer({ color: t.water, width: 0.5, }), filter: (z, f) => { return f.geomType === GeomType.Line && f.props.kind === "stream"; }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.pedestrian, }), filter: (z, f) => { return f.props.kind === "pedestrian"; }, }, { dataLayer: "landuse", symbolizer: new PolygonSymbolizer({ fill: t.pier, }), filter: (z, f) => { return f.props.kind === "pier"; }, }, { dataLayer: "buildings", symbolizer: new PolygonSymbolizer({ fill: t.buildings, opacity: 0.5, }), }, { dataLayer: "roads", symbolizer: new LineSymbolizer({ color: t.major, width: (z, f) => { return exp(1.6, [ [14, 0], [20, 7], ])(z); }, }), filter: (z, f) => { const kind = getString(f.props, "kind"); return ["other", "path"].includes(kind); }, }, { dataLayer: "roads", symbolizer: new LineSymbolizer({ color: t.major, width: (z, f) => { return exp(1.6, [ [13, 0], [18, 8], ])(z); }, }), filter: (z, f) => { return f.props.kind === "minor_road"; }, }, { dataLayer: "roads", symbolizer: new LineSymbolizer({ color: t.major, width: (z, f) => { return exp(1.6, [ [6, 0], [12, 1.6], [15, 3], [18, 13], ])(z); }, }), filter: (z, f) => { return f.props.kind === "major_road"; }, }, { dataLayer: "roads", symbolizer: new LineSymbolizer({ color: t.major, width: (z, f) => { return exp(1.6, [ [3, 0], [6, 1.1], [12, 1.6], [15, 5], [18, 15], ])(z); }, }), filter: (z, f) => { return f.props.kind === "highway"; }, }, { dataLayer: "boundaries", symbolizer: new LineSymbolizer({ color: t.boundaries, width: 1, }), filter: (z, f) => { const minAdminLevel = f.props.kind_detail; return typeof minAdminLevel === "number" && minAdminLevel <= 2; }, }, { dataLayer: "roads", symbolizer: new LineSymbolizer({ dash: [0.3, 0.75], color: t.railway, dashWidth: (z, f) => { return exp(1.6, [ [4, 0], [7, 0.15], [19, 9], ])(z); }, opacity: 0.5, }), filter: (z, f) => { return f.props.kind === "rail"; }, }, { dataLayer: "boundaries", symbolizer: new LineSymbolizer({ color: t.boundaries, width: 0.5, }), filter: (z, f) => { const minAdminLevel = f.props.kind_detail; return typeof minAdminLevel === "number" && minAdminLevel > 2; }, }, ]; }; export const labelRules = (t: Flavor, lang: string): LabelRule[] => { const nametags = [`name:${lang}`, "name"]; return [ // { // id: "neighbourhood", // dataLayer: "places", // symbolizer: languageStack( // new CenteredTextSymbolizer({ // labelProps: nametags, // fill: params.neighbourhoodLabel, // font: "500 10px sans-serif", // textTransform: "uppercase", // }), // params.neighbourhoodLabel, // ), // filter: (z, f) => { // return f.props["kind"] === "neighbourhood"; // }, // }, { dataLayer: "roads", symbolizer: new LineLabelSymbolizer({ labelProps: nametags, fill: t.roads_label_minor, font: "400 12px sans-serif", width: 2, stroke: t.roads_label_minor_halo, }), // TODO: sort by minzoom minzoom: 16, filter: (z, f) => { const kind = getString(f.props, "kind"); return ["minor_road", "other", "path"].includes(kind); }, }, { dataLayer: "roads", symbolizer: new LineLabelSymbolizer({ labelProps: nametags, fill: t.roads_label_major, font: "400 12px sans-serif", width: 2, stroke: t.roads_label_major_halo, }), // TODO: sort by minzoom minzoom: 12, filter: (z, f) => { const kind = getString(f.props, "kind"); return ["highway", "major_road"].includes(kind); }, }, { dataLayer: "roads", symbolizer: new LineLabelSymbolizer({ labelProps: nametags, fill: t.roads_label_major, font: "400 12px sans-serif", width: 2, stroke: t.roads_label_major_halo, }), // TODO: sort by minzoom minzoom: 12, filter: (z, f) => { const kind = getString(f.props, "kind"); return ["highway", "major_road"].includes(kind); }, }, { dataLayer: "water", symbolizer: new CenteredTextSymbolizer({ labelProps: nametags, fill: t.ocean_label, lineHeight: 1.5, letterSpacing: 1, font: (z, f) => { const size = linear([ [3, 10], [10, 12], ])(z); return `400 ${size}px sans-serif`; }, textTransform: "uppercase", }), filter: (z, f) => { const kind = getString(f.props, "kind"); return ( f.geomType === GeomType.Point && ["ocean", "bay", "strait", "fjord"].includes(kind) ); }, }, { dataLayer: "water", symbolizer: new CenteredTextSymbolizer({ labelProps: nametags, fill: t.ocean_label, lineHeight: 1.5, letterSpacing: 1, font: (z, f) => { const size = linear([ [3, 10], [6, 12], [10, 12], ])(z); return `400 ${size}px sans-serif`; }, }), filter: (z, f) => { const kind = getString(f.props, "kind"); return ( f.geomType === GeomType.Point && ["sea", "lake", "water"].includes(kind) ); }, }, { dataLayer: "places", symbolizer: new CenteredTextSymbolizer({ labelProps: (z, f) => { if (z < 6) { return [`ref:${lang}`, "ref"]; } return nametags; }, fill: t.state_label, stroke: t.state_label_halo, width: 1, lineHeight: 1.5, font: (z: number, f?: Feature) => { return "400 12px sans-serif"; }, textTransform: "uppercase", }), filter: (z, f) => { return f.props.kind === "region"; }, }, { dataLayer: "places", symbolizer: new CenteredTextSymbolizer({ labelProps: nametags, fill: t.country_label, lineHeight: 1.5, font: (z: number, f?: Feature) => { if (z < 6) return "600 12px sans-serif"; return "600 12px sans-serif"; }, textTransform: "uppercase", }), filter: (z, f) => { return f.props.kind === "country"; }, }, { // places_locality dataLayer: "places", minzoom: 9, symbolizer: new CenteredTextSymbolizer({ labelProps: nametags, fill: t.city_label, lineHeight: 1.5, font: (z: number, f?: Feature) => { if (!f) return "400 12px sans-serif"; const minZoom = f.props.min_zoom; let weight = 400; if (minZoom && minZoom <= 5) { weight = 600; } let size = 12; const popRank = f.props.population_rank; if (popRank && popRank > 9) { size = 16; } return `${weight} ${size}px sans-serif`; }, }), sort: (a, b) => { const aRank = getNumber(a, "min_zoom"); const bRank = getNumber(b, "min_zoom"); return aRank - bRank; }, filter: (z, f) => { return f.props.kind === "locality"; }, }, { dataLayer: "places", maxzoom: 8, symbolizer: new GroupSymbolizer([ new CircleSymbolizer({ radius: 2, fill: t.city_label, stroke: t.city_label_halo, width: 1.5, }), new OffsetTextSymbolizer({ labelProps: nametags, fill: t.city_label, stroke: t.city_label_halo, width: 1, offsetX: 6, offsetY: 4.5, font: (z, f) => { return "400 12px sans-serif"; }, }), ]), filter: (z, f) => { return f.props.kind === "locality"; }, }, ]; };