Back to Atlas map viewer
SOURCE / PINNED RELEASE

Made of little things.

Atlas map viewer

Release
1d2807e80b44…
Author-recorded commit
43810ed2b368…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-4757dbabc86

Archive hash verified: 8db6876d96c460ac…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

packages/renderer/src/features.ts
import type { Geometry, Position } from "geojson";
import type { MapFeature } from "../../features/src/index";
import { projectScreen, type Camera, type Point } from "./camera";
/** Canvas overlay preserves the complete source shapes; map and polygon holes share even-odd hit testing. */
export class FeatureOverlay {
  features: MapFeature[] = [];
  selected = new Set<string>();
  private hits: { feature: MapFeature; path: Path2D; area: boolean }[] = [];
  draw(ctx: CanvasRenderingContext2D, camera: Camera) {
    const screen = (v: Position) => projectScreen(v as Point, camera);
    this.hits = [];
    ctx.save();
    for (const f of [
      ...this.features.filter((f) => !this.selected.has(f.id)),
      ...this.features.filter((f) => this.selected.has(f.id)),
    ]) {
      const path = new Path2D(),
        area =
          f.geometry.type === "Polygon" || f.geometry.type === "MultiPolygon";
      const line = (v: Position[], closed = false) => {
        v.forEach((p, i) => {
          const [x, y] = screen(p);
          if (i === 0) path.moveTo(x!, y!);
          else path.lineTo(x!, y!);
        });
        if (closed) path.closePath();
      };
      const geometry = (g: Geometry): void => {
        switch (g.type) {
          case "LineString":
            line(g.coordinates);
            break;
          case "MultiLineString":
            g.coordinates.forEach((v) => line(v));
            break;
          case "Polygon":
            g.coordinates.forEach((v) => line(v, true));
            break;
          case "MultiPolygon":
            g.coordinates.forEach((p) => p.forEach((v) => line(v, true)));
            break;
        }
      };
      geometry(f.geometry);
      const selected = this.selected.has(f.id);
      ctx.fillStyle = selected ? "#da633d40" : "#327f991c";
      ctx.strokeStyle = selected ? "#bd4d2c" : "#327f99";
      ctx.lineWidth = selected ? 3 : 1.5;
      if (area) ctx.fill(path, "evenodd");
      ctx.stroke(path);
      this.hits.push({ feature: f, path, area });
    }
    ctx.restore();
  }
  pick(ctx: CanvasRenderingContext2D, x: number, y: number) {
    ctx.save();
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.lineWidth = 9;
    let id: string | undefined;
    for (const h of [...this.hits].reverse())
      if (
        (h.area && ctx.isPointInPath(h.path, x, y, "evenodd")) ||
        ctx.isPointInStroke(h.path, x, y)
      ) {
        id = h.feature.id;
        break;
      }
    ctx.restore();
    return id;
  }
}