Functional Weave
Code in Rust

geo.point-in-polygon@1.0.0

impl/typescript.ts

3,158 bytes · the TypeScript implementation · view raw

import { type GeoPoint } from "./geo_point_in_polygon_types.ts";

/**
 * Whether a point lies inside a polygon, by ray casting (the even-odd rule)
 * on the flat longitude/latitude plane.
 *
 * A point exactly on an edge or a vertex counts as inside: a delivery address
 * on the boundary line of a zone is in the zone. That is checked explicitly
 * first, because plain ray casting answers boundary points inconsistently
 * (inside on some edges, outside on others).
 *
 * Only + - * / and comparisons are used, in the same order in every language,
 * so the answer is identical in TypeScript, Python and Rust even where
 * floating point rounding decides it.
 */
export function pointInPolygon(point: GeoPoint, polygon: readonly GeoPoint[]): boolean {
  checkPoint(point);
  if (!Array.isArray(polygon)) throw new TypeError("polygon must be a list of points");
  polygon.forEach(checkPoint);

  let ring = polygon;
  // A closed ring repeats its first vertex at the end; that is not an edge.
  if (ring.length >= 2 && same(ring[0], ring[ring.length - 1])) ring = ring.slice(0, -1);
  if (countDistinct(ring) < 3) throw new RangeError("a polygon needs at least 3 distinct vertices");

  const n = ring.length;
  for (let i = 0; i < n; i++) {
    if (onSegment(point, ring[i], ring[(i + 1) % n])) return true;
  }

  let inside = false;
  for (let i = 0; i < n; i++) {
    const a = ring[i];
    const b = ring[(i + 1) % n];
    // Half-open test (one end strictly above, the other not) so a ray passing
    // exactly through a vertex counts it once, not twice.
    if (a.lat > point.lat !== b.lat > point.lat) {
      const crossLng = a.lng + ((point.lat - a.lat) * (b.lng - a.lng)) / (b.lat - a.lat);
      if (point.lng < crossLng) inside = !inside;
    }
  }
  return inside;
}

function onSegment(p: GeoPoint, a: GeoPoint, b: GeoPoint): boolean {
  const cross = (b.lng - a.lng) * (p.lat - a.lat) - (b.lat - a.lat) * (p.lng - a.lng);
  if (cross !== 0) return false;
  return (
    p.lng >= Math.min(a.lng, b.lng) &&
    p.lng <= Math.max(a.lng, b.lng) &&
    p.lat >= Math.min(a.lat, b.lat) &&
    p.lat <= Math.max(a.lat, b.lat)
  );
}

function same(a: GeoPoint, b: GeoPoint): boolean {
  return a.lat === b.lat && a.lng === b.lng;
}

function countDistinct(ring: readonly GeoPoint[]): number {
  let count = 0;
  for (let i = 0; i < ring.length; i++) {
    let seen = false;
    for (let j = 0; j < i; j++) {
      if (same(ring[i], ring[j])) {
        seen = true;
        break;
      }
    }
    if (!seen) count++;
  }
  return count;
}

function checkPoint(p: GeoPoint): void {
  if (p === null || typeof p !== "object") throw new TypeError("a point must have lat and lng");
  if (typeof p.lat !== "number" || !Number.isFinite(p.lat)) throw new TypeError("latitude must be a finite number of degrees");
  if (typeof p.lng !== "number" || !Number.isFinite(p.lng)) throw new TypeError("longitude must be a finite number of degrees");
  if (p.lat < -90 || p.lat > 90) throw new RangeError("latitude must be between -90 and 90 degrees");
  if (p.lng < -180 || p.lng > 180) throw new RangeError("longitude must be between -180 and 180 degrees");
}