Use local PMTiles basemap

This commit is contained in:
2026-07-25 21:55:35 -05:00
commit 4532a5c5dd
37 changed files with 4019 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
const FEET_PER_METER = 3.280839895;
const METERS_PER_FOOT = 1 / FEET_PER_METER;
const EARTH_METERS_PER_DEGREE_LAT = 111320;
function toRadians(value) {
return (value * Math.PI) / 180;
}
function rotateLocalToTrueNorthEast(northMeters, eastMeters, assumedNorthClockwiseDegrees = 0) {
const clockwiseRadians = toRadians(assumedNorthClockwiseDegrees);
const trueNorthMeters =
northMeters * Math.cos(clockwiseRadians) - eastMeters * Math.sin(clockwiseRadians);
const trueEastMeters =
northMeters * Math.sin(clockwiseRadians) + eastMeters * Math.cos(clockwiseRadians);
return {
northMeters: trueNorthMeters,
eastMeters: trueEastMeters,
};
}
export function point(lat, lon) {
return { lat, lon };
}
export function lngLat(lon, lat) {
return point(lat, lon);
}
export function anchor(lon, lat, options = {}) {
return {
...lngLat(lon, lat),
...options,
};
}
export function pointsFromLngLat(coordinates) {
return coordinates.map(([lon, lat]) => lngLat(lon, lat));
}
export function ft(value) {
return value * METERS_PER_FOOT;
}
function isPoint(value) {
return Boolean(value) && typeof value.lat === "number" && typeof value.lon === "number";
}
function isMove(value) {
return (
Boolean(value) &&
typeof value.northMeters === "number" &&
typeof value.eastMeters === "number"
);
}
function isOffsetAnchor(value) {
return Boolean(value) && value.kind === "offset-anchor" && isPoint(value.origin);
}
function translate(origin, northMeters, eastMeters) {
const latDelta = northMeters / EARTH_METERS_PER_DEGREE_LAT;
const lonDelta = eastMeters / (EARTH_METERS_PER_DEGREE_LAT * Math.cos(toRadians(origin.lat)));
return point(origin.lat + latDelta, origin.lon + lonDelta);
}
export function offset(origin, ...moves) {
if (moves.length === 0) {
return { kind: "offset-anchor", origin };
}
const net = moves.reduce(
(accumulator, move) => ({
northMeters: accumulator.northMeters + move.northMeters,
eastMeters: accumulator.eastMeters + move.eastMeters,
}),
{ northMeters: 0, eastMeters: 0 },
);
const adjusted = rotateLocalToTrueNorthEast(
net.northMeters,
net.eastMeters,
origin.assumedNorthClockwiseDegrees ?? 0,
);
return translate(origin, adjusted.northMeters, adjusted.eastMeters);
}
export function north(distanceMeters) {
return { northMeters: distanceMeters, eastMeters: 0 };
}
export function south(distanceMeters) {
return { northMeters: -distanceMeters, eastMeters: 0 };
}
export function east(distanceMeters) {
return { northMeters: 0, eastMeters: distanceMeters };
}
export function west(distanceMeters) {
return { northMeters: 0, eastMeters: -distanceMeters };
}
function buildFeature(type, geometryType, style = {}) {
const coordinates = [];
return {
add(...values) {
coordinates.push(resolveCoordinate(values));
return this;
},
toGeoJSON() {
if (geometryType === "Point") {
const [first] = coordinates;
return {
type: "Feature",
properties: { type, style },
geometry: {
type: "Point",
coordinates: [first.lon, first.lat],
},
};
}
const ring = coordinates.map((coordinate) => [coordinate.lon, coordinate.lat]);
const geometry =
geometryType === "LineString"
? { type: "LineString", coordinates: ring }
: { type: "Polygon", coordinates: [closeRing(ring)] };
return {
type: "Feature",
properties: { type, style },
geometry,
};
},
};
}
function resolveCoordinate(values) {
if (values.length === 1 && isPoint(values[0])) {
return values[0];
}
const [first, ...rest] = values;
if (isOffsetAnchor(first) && rest.every(isMove)) {
return offset(first.origin, ...rest);
}
throw new Error("add(...) expects a point, or offset(origin) followed by directional moves");
}
function closeRing(coordinates) {
if (coordinates.length === 0) {
return coordinates;
}
const [firstLon, firstLat] = coordinates[0];
const [lastLon, lastLat] = coordinates[coordinates.length - 1];
if (firstLon === lastLon && firstLat === lastLat) {
return coordinates;
}
return [...coordinates, coordinates[0]];
}
export function polygon(type, style) {
return buildFeature(type, "Polygon", style);
}
export function path(type, style) {
return buildFeature(type, "LineString", style);
}
export function marker(type, style) {
return buildFeature(type, "Point", style);
}
export function sprinkler(name) {
return marker(name, {
color: "#2d7dd2",
radius: 5,
fillColor: "#88bdf2",
fillOpacity: 0.95,
});
}
export function tree(name) {
return marker(name, {
color: "#2f6b3d",
radius: 9,
fillColor: "#5c9e64",
fillOpacity: 0.85,
});
}
export function collectGeoJSON(...collections) {
return {
type: "FeatureCollection",
features: collections.flat().map((entry) => entry.toGeoJSON()),
};
}