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/overlays.ts
import { cacheCoverage } from "./cache-coverage";
import {
  project,
  unproject,
  type Peak,
  type Place,
} from "../../format/src/index";
import type {
  Position,
  WalkingResult,
  Reachable,
} from "../../routing/src/index";
import type { MapStyle } from "./style";
import { worldProjector, worldToScreen, type Camera } from "./camera";
import type { MapLabels, LabelStats } from "./labels";
import type { FeatureOverlay } from "./features";
import type { TileView } from "./tile-presentation";
export interface OverlayFrame {
  ctx: CanvasRenderingContext2D;
  camera: Camera;
  style: MapStyle;
  view: TileView;
  resident: (key: string) => boolean;
  cached: (key: string) => boolean;
  labels: MapLabels;
  features: FeatureOverlay;
  places: Place[];
  placesLoading: boolean;
  placesMissing: boolean;
  state: {
    routingCells: Array<{ key: string; state: string }>;
    reachable: Reachable | null;
    selection: { points: Position[]; closed: boolean } | null;
    grid: boolean;
    cache: boolean;
    route: Extract<WalkingResult, { status: "found" }> | null;
    endpoints: [Position | null, Position | null];
    location: {
      coordinates: Position;
      name: string;
      street?: Position[];
      requested?: Position;
    } | null;
    selected: Peak | null;
  };
  labelStats: LabelStats;
}
export interface MapOverlay {
  id: string;
  order: number;
  draw(frame: OverlayFrame): void;
  dispose?(): void;
}
/** Stable ordered drawing with canvas isolation and explicit replacement/disposal. */
export class OverlayRegistry {
  private layers: MapOverlay[] = [];
  add(layer: MapOverlay) {
    this.remove(layer.id);
    this.layers.push(layer);
    this.layers.sort((a, b) => a.order - b.order);
    return () => {
      if (this.layers.includes(layer)) this.remove(layer.id);
    };
  }
  remove(id: string) {
    const layer = this.layers.find((l) => l.id === id);
    this.layers = this.layers.filter((l) => l.id !== id);
    layer?.dispose?.();
  }
  draw(frame: OverlayFrame) {
    for (const layer of this.layers) {
      frame.ctx.save();
      try {
        layer.draw(frame);
      } finally {
        frame.ctx.restore();
      }
    }
  }
  destroy() {
    for (const layer of this.layers) layer.dispose?.();
    this.layers = [];
  }
}
export function defaultOverlays() {
  const registry = new OverlayRegistry();
  registry.add({
    id: "routing-cells",
    order: 0,
    draw: (frame) => {
      const { ctx, camera, state } = frame;
      const { width, height } = camera;
      for (const c of state.routingCells) {
        const parts = c.key.split("/").slice(-3),
          z = Number(parts[0]),
          x = Number(parts[1]),
          y = Number(parts[2]),
          n = 2 ** z;
        const corners = [
          [x / n, y / n],
          [(x + 1) / n, y / n],
          [(x + 1) / n, (y + 1) / n],
          [x / n, (y + 1) / n],
        ].map((p) => worldToScreen(p as [number, number], camera));
        if (
          corners.every((p) => p[0] < 0) ||
          corners.every((p) => p[0] > width) ||
          corners.every((p) => p[1] < 0) ||
          corners.every((p) => p[1] > height)
        )
          continue;
        const color =
          (
            {
              cached: "#397657",
              loaded: "#397ea2",
              loading: "#bb842f",
              missing: "#c34d40",
            } as Record<string, string>
          )[c.state] ?? "#397657";
        ctx.beginPath();
        corners.forEach(([x, y], i) =>
          i ? ctx.lineTo(x, y) : ctx.moveTo(x, y),
        );
        ctx.closePath();
        ctx.fillStyle = color + "16";
        ctx.fill();
        ctx.strokeStyle = color + "b0";
        ctx.lineWidth = 1.5;
        ctx.stroke();
      }
    },
  });
  registry.add({
    id: "reachability",
    order: 10,
    draw: (frame) => {
      const { ctx, camera, state } = frame;
      const toScreen = worldProjector(camera);
      const screen = (lon: number, lat: number) => toScreen(project(lon, lat));
      if (state.reachable) {
        const colors = ["#22776f", "#578fac", "#c89949", "#a479ae"];
        for (
          let i = state.reachable.contours.features.length - 1;
          i >= 0;
          i--
        ) {
          const feature = state.reachable.contours.features[i]!;
          ctx.beginPath();
          for (const polygon of feature.geometry.coordinates)
            for (const ring of polygon) {
              ring.forEach((p, j) => {
                const [x, y] = screen(p[0]!, p[1]!);
                if (j) ctx.lineTo(x, y);
                else ctx.moveTo(x, y);
              });
              ctx.closePath();
            }
          ctx.fillStyle = colors[i] + "35";
          ctx.fill("evenodd");
          ctx.strokeStyle = colors[i]!;
          ctx.lineWidth = 1.5;
          ctx.stroke();
        }
        const [x, y] = screen(...state.reachable.start.coordinates);
        ctx.beginPath();
        ctx.arc(x, y, 7, 0, Math.PI * 2);
        ctx.fillStyle = "#22776f";
        ctx.fill();
        ctx.strokeStyle = "#fff";
        ctx.lineWidth = 3;
        ctx.stroke();
      }
    },
  });
  registry.add({
    id: "selection",
    order: 20,
    draw: (frame) => {
      const { ctx, camera, state } = frame;
      const toScreen = worldProjector(camera);
      const screen = (lon: number, lat: number) => toScreen(project(lon, lat));
      if (state.selection?.points.length) {
        ctx.beginPath();
        state.selection.points.forEach((p, i) => {
          const [x, y] = screen(...p);
          if (i === 0) ctx.moveTo(x!, y!);
          else ctx.lineTo(x!, y!);
        });
        if (state.selection.closed) ctx.closePath();
        ctx.fillStyle = "#507b4325";
        ctx.fill();
        ctx.strokeStyle = "#345d48";
        ctx.lineWidth = 2.5;
        ctx.setLineDash(state.selection.closed ? [] : [6, 4]);
        ctx.stroke();
        ctx.setLineDash([]);
        for (const p of state.selection.points) {
          const [x, y] = screen(...p);
          ctx.beginPath();
          ctx.arc(x!, y!, 4, 0, Math.PI * 2);
          ctx.fillStyle = "#fbfaf5";
          ctx.fill();
          ctx.stroke();
        }
      }
    },
  });
  registry.add({
    id: "tile-grid",
    order: 30,
    draw: (frame) => {
      const { ctx, camera, style, state, view } = frame;
      if (state.grid) {
        ctx.lineWidth = 1;
        ctx.font = "10px monospace";
        for (const b of view.blocks) {
          const n = 2 ** b.z!;
          const corners = [
            [b.x! / n, b.y! / n],
            [(b.x! + 1) / n, b.y! / n],
            [(b.x! + 1) / n, (b.y! + 1) / n],
            [b.x! / n, (b.y! + 1) / n],
          ].map((p) => worldToScreen(p as [number, number], camera));
          ctx.strokeStyle = frame.resident(b.key) ? "#65897688" : "#c65b47";
          ctx.beginPath();
          corners.forEach(([x, y], i) =>
            i ? ctx.lineTo(x, y) : ctx.moveTo(x, y),
          );
          ctx.closePath();
          ctx.stroke();
          ctx.fillStyle = style.labels.color;
          ctx.fillText(
            `${b.z}/${b.x}/${b.y}`,
            corners[0]![0] + 6,
            corners[0]![1] + 15,
          );
        }
      }
    },
  });
  registry.add({
    id: "labels",
    order: 40,
    draw: (frame) => {
      const { ctx, camera, style } = frame;
      const { width, height } = camera;
      const toScreen = worldProjector(camera);
      const screen = (lon: number, lat: number) => toScreen(project(lon, lat));
      ctx.font = `500 ${12 * style.labels.scale}px "Avenir Next", sans-serif`;
      ctx.textAlign = "center";
      frame.features.draw(ctx, camera);
      const occupied: Array<[number, number]> = [];
      for (const place of frame.places) {
        const [x, y] = screen(...place.coordinates);
        if (
          x! < 25 ||
          y! < 25 ||
          x! > width - 25 ||
          y! > height - 25 ||
          occupied.some(
            ([a, b]) =>
              Math.abs(a - x!) < 85 * style.labels.scale &&
              Math.abs(b - y!) < 23 * style.labels.scale,
          )
        )
          continue;
        occupied.push([x!, y!]);
        ctx.strokeStyle = style.labels.halo;
        ctx.lineWidth = 3;
        ctx.strokeText(place.name, x!, y!);
        ctx.fillStyle = style.labels.color;
        ctx.fillText(place.name, x!, y!);
      }
      frame.labelStats = frame.labels.draw(
        ctx,
        camera,
        style.labels,
        occupied.map(([x, y]) => [
          x - 44 * style.labels.scale,
          y - 12 * style.labels.scale,
          x + 44 * style.labels.scale,
          y + 5 * style.labels.scale,
        ]),
      );
      frame.labelStats.places = occupied.length;
      frame.labelStats.loading += frame.placesLoading ? 1 : 0;
      frame.labelStats.missing += frame.placesMissing ? 1 : 0;
    },
  });
  registry.add({
    id: "route",
    order: 50,
    draw: (frame) => {
      const { ctx, camera, state } = frame;
      const toScreen = worldProjector(camera);
      const screen = (lon: number, lat: number) => toScreen(project(lon, lat));
      if (state.route) {
        ctx.lineJoin = "round";
        ctx.lineCap = "round";
        ctx.beginPath();
        state.route.coordinates.forEach((p, i) => {
          const [x, y] = screen(...p);
          if (i === 0) ctx.moveTo(x!, y!);
          else ctx.lineTo(x!, y!);
        });
        ctx.strokeStyle = "#fff8e9";
        ctx.lineWidth = 8;
        ctx.stroke();
        ctx.strokeStyle = "#be5438";
        ctx.lineWidth = 4;
        ctx.stroke();
        // Dotted offsets show snapping, not a claimed walkable connection.
        [state.route.start.coordinates, state.route.end.coordinates].forEach(
          (p, i) => {
            const request = state.endpoints[i];
            if (!request) return;
            const [x, y] = screen(...request),
              [sx, sy] = screen(...p);
            ctx.beginPath();
            ctx.moveTo(x!, y!);
            ctx.lineTo(sx!, sy!);
            ctx.strokeStyle = "#be5438";
            ctx.lineWidth = 1.5;
            ctx.setLineDash([3, 4]);
            ctx.stroke();
            ctx.setLineDash([]);
          },
        );
      }
      state.endpoints.forEach((p, i) => {
        if (!p) return;
        const [x, y] = screen(...p);
        ctx.beginPath();
        ctx.arc(x!, y!, 13, 0, Math.PI * 2);
        ctx.fillStyle = i === 0 ? "#345d48" : "#be5438";
        ctx.fill();
        ctx.strokeStyle = "#fff8e9";
        ctx.lineWidth = 3;
        ctx.stroke();
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        ctx.fillStyle = "#fff8e9";
        ctx.font = 'bold 12px "Avenir Next", sans-serif';
        ctx.fillText(i === 0 ? "A" : "B", x!, y!);
        ctx.textBaseline = "alphabetic";
      });
    },
  });
  registry.add({
    id: "location",
    order: 60,
    draw: (frame) => {
      const { ctx, camera, state } = frame;
      const toScreen = worldProjector(camera);
      const screen = (lon: number, lat: number) => toScreen(project(lon, lat));
      if (state.location) {
        const selected = state.location;
        if (selected.street) {
          ctx.beginPath();
          selected.street.forEach((p, i) => {
            const [x, y] = screen(...p);
            if (i === 0) ctx.moveTo(x!, y!);
            else ctx.lineTo(x!, y!);
          });
          ctx.strokeStyle = "#fff9e9";
          ctx.lineWidth = 8;
          ctx.stroke();
          ctx.strokeStyle = "#39708a";
          ctx.lineWidth = 4;
          ctx.stroke();
        }
        const [x, y] = screen(...selected.coordinates);
        if (selected.requested) {
          const [rx, ry] = screen(...selected.requested);
          ctx.beginPath();
          ctx.moveTo(rx!, ry!);
          ctx.lineTo(x!, y!);
          ctx.setLineDash([3, 4]);
          ctx.lineWidth = 2;
          ctx.strokeStyle = "#39708a";
          ctx.stroke();
          ctx.setLineDash([]);
          ctx.beginPath();
          ctx.arc(rx!, ry!, 5, 0, Math.PI * 2);
          ctx.fillStyle = "#fff9e9";
          ctx.fill();
          ctx.stroke();
        }
        ctx.beginPath();
        ctx.arc(x!, y!, 7, 0, Math.PI * 2);
        ctx.fillStyle = "#39708a";
        ctx.fill();
        ctx.strokeStyle = "#fff9e9";
        ctx.lineWidth = 3;
        ctx.stroke();
        ctx.font = '600 12px "Avenir Next", sans-serif';
        ctx.textAlign = "center";
        ctx.strokeText(selected.name, x!, y! - 17);
        ctx.fillStyle = "#254e63";
        ctx.fillText(selected.name, x!, y! - 17);
      }
    },
  });
  registry.add({
    id: "peak",
    order: 70,
    draw: (frame) => {
      const { ctx, camera, state } = frame;
      const toScreen = worldProjector(camera);
      const screen = (lon: number, lat: number) => toScreen(project(lon, lat));
      if (state.selected) {
        const [x, y] = screen(...state.selected.coordinates);
        ctx.beginPath();
        ctx.arc(x!, y!, 12, 0, Math.PI * 2);
        ctx.fillStyle = "#d15b37";
        ctx.fill();
        ctx.strokeStyle = "#fff8e9";
        ctx.lineWidth = 3;
        ctx.stroke();
        ctx.font = '600 13px "Avenir Next", sans-serif';
        ctx.textAlign = "center";
        ctx.strokeText(state.selected.name, x!, y! - 23);
        ctx.fillStyle = "#693923";
        ctx.fillText(state.selected.name, x!, y! - 23);
      }
    },
  });
  registry.add({
    id: "scale",
    order: 80,
    draw: (frame) => {
      const { ctx, camera, style } = frame;
      const { height } = camera;
      const world = 256 * 2 ** camera.zoom;
      const [, lat] = unproject(...camera.center);
      const metersPerPixel =
        (Math.cos((lat * Math.PI) / 180) * 40075016.686) / world;
      const ideal = 110 * metersPerPixel;
      const base = 10 ** Math.floor(Math.log10(ideal));
      const distance =
        [1, 2, 5, 10]
          .map((n) => n * base)
          .filter((n) => n <= ideal)
          .pop() ?? base;
      const pixels = distance / metersPerPixel;
      ctx.strokeStyle = style.labels.color;
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(24, height - 39);
      ctx.lineTo(24, height - 34);
      ctx.lineTo(24 + pixels, height - 34);
      ctx.lineTo(24 + pixels, height - 39);
      ctx.stroke();
      ctx.font = "10px monospace";
      ctx.textAlign = "left";
      ctx.fillStyle = style.labels.color;
      ctx.fillText(
        distance >= 1000 ? `${distance / 1000} km` : `${distance} m`,
        24,
        height - 44,
      );
    },
  });
  registry.add({
    id: "cache-coverage",
    order: 25,
    draw: ({ ctx, camera, view, cached, state }) => {
      if (!state.cache) return;
      const cells = cacheCoverage(view, cached);
      for (const c of cells) {
        const n = 2 ** c.z;
        const corners = [
          [c.x / n, c.y / n],
          [(c.x + 1) / n, c.y / n],
          [(c.x + 1) / n, (c.y + 1) / n],
          [c.x / n, (c.y + 1) / n],
        ].map((p) => worldToScreen(p as [number, number], camera));
        ctx.beginPath();
        corners.forEach(([x, y], i) =>
          i ? ctx.lineTo(x, y) : ctx.moveTo(x, y),
        );
        ctx.closePath();
        ctx.fillStyle =
          c.fraction === null || !c.total
            ? "rgba(100,110,120,0.07)"
            : `rgba(15, 95, 240, ${0.04 + 0.45 * c.fraction})`;
        ctx.fill();
        ctx.strokeStyle = "rgba(25,78,146,0.65)";
        ctx.lineWidth = 1;
        ctx.setLineDash(c.fraction === null ? [4, 4] : []);
        ctx.stroke();
        const [x, y] = worldToScreen(
          [(c.x + 0.5) / n, (c.y + 0.5) / n],
          camera,
        );
        const label =
          c.fraction === null
            ? "? directory unknown"
            : !c.total
              ? "∅ no blocks"
              : `${Math.round(c.fraction * 100)}% · ${c.cached}/${c.total}`;
        ctx.font = "11px monospace";
        ctx.textAlign = "center";
        ctx.lineWidth = 3;
        ctx.strokeStyle = "#fff";
        ctx.strokeText(label, x, y);
        ctx.fillStyle = "#104586";
        ctx.fillText(label, x, y);
      }
    },
  });
  return registry;
}