204 lines
4.9 KiB
JavaScript
204 lines
4.9 KiB
JavaScript
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()),
|
|
};
|
|
} |