aboutsummaryrefslogtreecommitdiffstats
path: root/app/geometry.js
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-04 17:07:24 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-09 22:48:55 +0200
commitbe7ec90b500ac68e053f2b58feb085247ef95817 (patch)
treeaef7732ce0bbe505c6bc8486e1d0da2c06990e6a /app/geometry.js
parenta4ed99a370930b1a0c0f065906ed99c15a015fd4 (diff)
downloadhousing-be7ec90b500ac68e053f2b58feb085247ef95817.tar.zst
Refactor application to use couchbase
Diffstat (limited to 'app/geometry.js')
-rw-r--r--app/geometry.js173
1 files changed, 149 insertions, 24 deletions
diff --git a/app/geometry.js b/app/geometry.js
index 62236e0..ac6b46c 100644
--- a/app/geometry.js
+++ b/app/geometry.js
@@ -9,7 +9,7 @@ const R = 6371000;
const TOLERANCE = 1e-10;
/**
- * Geographic bounds representation
+ * Geographic bounds representation for Bound caluclation typing
*/
export class Bounds {
/**
@@ -134,10 +134,9 @@ export class Geometry {
/**
* Deserialize geometry from GeoJSON
* @param {{type: string, coordinates?: any, [key: string]: any}} geojson
- * @returns {Geometry|null}
+ * @returns {Point|LineString|Polygon|MultiLineString}
*/
static fromGeoJSON(geojson) {
- if (!geojson?.type) return null;
switch (geojson.type) {
case "Point":
return Point.fromGeoJSON(geojson);
@@ -147,6 +146,8 @@ export class Geometry {
return Polygon.fromGeoJSON(geojson);
case "MultiLineString":
return MultiLineString.fromGeoJSON(geojson);
+ case "MultiPolygon":
+ return Polygon.fromGeoJSON(geojson);
default:
throw new Error(`Invalid GeoJSON object: missing required 'type' property`);
}
@@ -154,7 +155,7 @@ export class Geometry {
/**
* Compute geometry bounds
- * @param {Geometry} geometry - Input geometry
+ * @param {Point|LineString|Polygon} geometry - Input geometry
* @returns {Bounds} Geometry bounds
*/
static bounds(geometry) {
@@ -169,8 +170,7 @@ export class Geometry {
if (geometry instanceof Polygon) {
return Geometry.calculateBoundsFromCoords(geometry.rings[0]);
}
-
- throw new Error(`Unsupported geometry type: ${geometry.type}`);
+ throw new Error(`Unsupported geometry type: ${geometry}`);
}
/**
@@ -186,6 +186,97 @@ export class Geometry {
}
/**
+ * Calculate perpendicular distance from a point to a great-circle segment
+ * @param {Point} point - Point to measure distance from
+ * @param {Point} lineStart - Start point of the line segment
+ * @param {Point} lineEnd - End point of the line segment
+ * @returns {number} Perpendicular distance in meters
+ */
+ static #perpendicularDistance(point, lineStart, lineEnd) {
+ // If start and end are the same, return distance to the point
+ if (lineStart.equals(lineEnd)) {
+ return Point.distance(point, lineStart);
+ }
+
+ // Convert coordinates to radians
+ const φ1 = (lineStart.lat * Math.PI) / 180;
+ const λ1 = (lineStart.lng * Math.PI) / 180;
+ const φ2 = (lineEnd.lat * Math.PI) / 180;
+ const λ2 = (lineEnd.lng * Math.PI) / 180;
+ const φ = (point.lat * Math.PI) / 180;
+ const λ = (point.lng * Math.PI) / 180;
+
+ // Calculate great-circle distance from start to end
+ const segmentLength = Point.distance(lineStart, lineEnd);
+ if (segmentLength === 0) {
+ return Point.distance(point, lineStart);
+ }
+
+ // Calculate bearing from lineStart to point and lineStart to lineEnd
+ const Δλ = λ2 - λ1;
+ const y = Math.sin(Δλ) * Math.cos(φ2);
+ const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
+ const bearing1 = Math.atan2(y, x);
+
+ const y2 = Math.sin(λ - λ1) * Math.cos(φ);
+ const x2 = Math.cos(φ1) * Math.sin(φ) - Math.sin(φ1) * Math.cos(φ) * Math.cos(λ - λ1);
+ const bearing2 = Math.atan2(y2, x2);
+
+ // Cross-track distance (approximation for small distances)
+ const crossTrack = Math.asin(
+ Math.sin(Point.distance(lineStart, point) / R) * Math.sin(bearing2 - bearing1),
+ );
+ const distance = Math.abs(R * crossTrack);
+
+ // Check if the closest point is within the segment
+ const alongTrack =
+ Math.acos(Math.cos(Point.distance(lineStart, point) / R) / Math.cos(distance / R)) * R;
+
+ if (alongTrack < 0 || alongTrack > segmentLength) {
+ // Return distance to closest endpoint
+ return Math.min(Point.distance(point, lineStart), Point.distance(point, lineEnd));
+ }
+
+ return distance;
+ }
+
+ /**
+ * Simplify geometry using Ramer-Douglas-Peucker algorithm
+ * @param {Coordinate[]} coords - Array of coordinates to simplify
+ * @param {number} epsilon - Maximum distance tolerance in meters
+ * @returns {Coordinate[]} Simplified coordinate array
+ */
+ static simplifyCoords(coords, epsilon) {
+ if (coords.length <= 2) return coords;
+
+ let dmax = 0;
+ let index = 0;
+
+ // Find the point with the maximum perpendicular distance
+ const startPoint = new Point(coords[0][0], coords[0][1]);
+ const endPoint = new Point(coords[coords.length - 1][0], coords[coords.length - 1][1]);
+
+ for (let i = 1; i < coords.length - 1; i++) {
+ const point = new Point(coords[i][0], coords[i][1]);
+ const d = Geometry.#perpendicularDistance(point, startPoint, endPoint);
+ if (d > dmax) {
+ dmax = d;
+ index = i;
+ }
+ }
+
+ // If max distance is greater than epsilon, recursively simplify
+ if (dmax > epsilon) {
+ const recResults1 = Geometry.simplifyCoords(coords.slice(0, index + 1), epsilon);
+ const recResults2 = Geometry.simplifyCoords(coords.slice(index), epsilon);
+ // Combine results, excluding the duplicate point at index
+ return [...recResults1.slice(0, -1), ...recResults2];
+ } else {
+ return [coords[0], coords[coords.length - 1]];
+ }
+ }
+
+ /**
* Calculate distance between geometries
* @param {Geometry} a - First geometry
* @param {Geometry} b - Second geometry
@@ -515,7 +606,6 @@ export class Geometry {
}
}
-/** Point geometry class */
export class Point extends Geometry {
/**
* @param {number} lng - Longitude
@@ -579,7 +669,6 @@ export class Point extends Geometry {
}
}
-/** MultiLineString geometry class */
export class MultiLineString extends Geometry {
/**
* @param {Coordinate[][]} coordinates - Line coordinates
@@ -606,6 +695,19 @@ export class MultiLineString extends Geometry {
};
}
+ /**
+ * Simplify MultiLineString using Ramer-Douglas-Peucker algorithm
+ * @param {number} epsilon - Maximum distance tolerance in meters
+ * @returns {MultiLineString} Simplified MultiLineString
+ */
+ simplify(epsilon) {
+ if (epsilon < 0) {
+ throw new Error("Epsilon must be non-negative");
+ }
+ const simplifiedCoords = this.coordinates.map((line) => Geometry.simplifyCoords(line, epsilon));
+ return new MultiLineString(simplifiedCoords);
+ }
+
/** @returns {Bounds} Geometry bounds */
bounds() {
const allCoords = this.coordinates.flat();
@@ -613,7 +715,6 @@ export class MultiLineString extends Geometry {
}
}
-/** LineString geometry class */
export class LineString extends Geometry {
/**
* @param {Coordinate[]} coordinates - Line coordinates
@@ -624,6 +725,17 @@ export class LineString extends Geometry {
}
/**
+ * @param {number} epsilon - Line coordinates
+ */
+ simplify(epsilon) {
+ if (epsilon < 0) {
+ throw new Error("Epsilon must be non-negative");
+ }
+ const simplifiedCoords = Geometry.simplifyCoords(this.coordinates, epsilon);
+ return new LineString(simplifiedCoords);
+ }
+
+ /**
* Check if two lines intersect
* @param {LineString} line1 - First line
* @param {LineString} line2 - Second line
@@ -798,7 +910,6 @@ export class LineString extends Geometry {
}
}
-/** Polygon geometry class */
export class Polygon extends Geometry {
/**
* @param {Coordinate[][]} rings - Polygon rings (first is exterior, rest are holes)
@@ -857,6 +968,29 @@ export class Polygon extends Geometry {
}
/**
+ * Simplify Polygon using Ramer-Douglas-Peucker algorithm
+ * @param {number} epsilon - Maximum distance tolerance in meters
+ * @returns {Polygon} Simplified Polygon
+ */
+ simplify(epsilon) {
+ if (epsilon < 0) {
+ throw new Error("Epsilon must be non-negative");
+ }
+ const simplifiedRings = this.rings.map((ring) => {
+ const simplified = Geometry.simplifyCoords(ring, epsilon);
+ if (
+ simplified.length > 2 &&
+ (simplified[0][0] !== simplified[simplified.length - 1][0] ||
+ simplified[0][1] !== simplified[simplified.length - 1][1])
+ ) {
+ simplified.push(simplified[0]);
+ }
+ return simplified;
+ });
+ return new Polygon(simplifiedRings);
+ }
+
+ /**
* Create from GeoJSON
* @param {Object} geojson - GeoJSON Polygon
* @returns {Polygon}
@@ -892,15 +1026,13 @@ export class Polygon extends Geometry {
}
}
-/** GeoJSON Feature class */
export class Feature {
/**
* @param {Geometry} geometry - Feature geometry
* @param {Object} [properties={}] - Feature properties
* @param {string|number} [id] - Feature ID
*/
- constructor(geometry, properties = {}, id = null) {
- this.type = "Feature";
+ constructor(geometry, properties = {}, id = "") {
this.geometry = geometry;
this.properties = properties;
this.id = id;
@@ -924,27 +1056,21 @@ export class Feature {
* @param {Object} geojson - GeoJSON feature
* @returns {Feature|null}
*/
- /**
- * @param {{geometry: object, properties?: object, id?: string|number}} geojson
- * @returns {Feature|null}
- */
static fromGeoJSON(geojson) {
if (!geojson?.geometry) return null;
const geometry = Geometry.fromGeoJSON(geojson.geometry);
if (!geometry) return null;
- return new Feature(geometry, geojson.properties ?? {}, geojson.id ?? null);
+ return new Feature(geometry, geojson.properties ?? {}, geojson.id ?? "");
}
}
-/** GeoJSON Feature Collection class */
export class Collection {
/**
- * @param {Feature[]} [features=[]] - Feature array
+ * @param {Feature[]} features - Feature array
*/
constructor(features = []) {
- this.type = "FeatureCollection";
this.features = features;
}
@@ -965,12 +1091,11 @@ export class Collection {
* @returns {Collection}
*/
/**
- * @param {{features?: any[]}} geojson
+ * @param {{features: any[]}} geojson
* @returns {Collection}
*/
static fromGeoJSON(geojson) {
- const features = (geojson.features ?? []).map(Feature.fromGeoJSON).filter(Boolean);
-
+ const features = geojson.features.map(Feature.fromGeoJSON).filter((x) => x !== null);
return new Collection(features);
}