Ctrl K
ComunidadGuías…

src/lib/map-camera.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, expect, it } from "vitest";

import { MAX_FIT_ZOOM, MIN_ZOOM } from "@/lib/basemap";
import {
  frameCamera,
  projectFrom,
  tileLayout,
  worldCamera,
  type MapFrame,
} from "@/lib/map-camera";
import { bandIn, mapCanvas, placedLocations } from "@/lib/map-frame";
import { tileCenter, TILE_SIZE } from "@/lib/map-grid";

// The arithmetic a card and a hero compose their maps with, short of any
// layout: which tiles a frame asks for, where they and the pins go, and that
// nothing past the grid is asked for. Whether the fit agrees with GL JS is
// `map-camera.browser.test.ts`'s question, and whether the composed map lands
// where it should in a real card is `card-frames.browser.test.tsx`'s.

const card = (width: number, height = 236): MapFrame => ({
  width,
  height,
  band: bandIn(height, 20, 80),
  inset: mapCanvas(width, false).inset,
});

const BLUE_HOLE = placedLocations([
  { name: "Blue Hole", latitude: 28.5721, longitude: 34.5372 },
]);

// What `area` covers, relative to the camera's centre, in a frame showing the
// centre at `anchor`.
const whole = (frame: MapFrame, anchor: { x: number; y: number }) => ({
  left: -anchor.x,
  top: -anchor.y,
  right: frame.width - anchor.x,
  bottom: frame.height - anchor.y,
});

describe("frameCamera", () => {
  it("opens a lone place at the deepest a map is fitted at", () => {
    expect(frameCamera(BLUE_HOLE, card(252)).zoom).toBe(MAX_FIT_ZOOM);
  });

  it("shows nothing placed as the whole world, on the equator", () => {
    expect(frameCamera([], card(252))).toEqual({
      center: { latitude: 0, longitude: 0 },
      zoom: MIN_ZOOM,
    });
  });

  it("fits a wider frame at least as deep as a narrower one", () => {
    const places = placedLocations([
      { latitude: 28.5721, longitude: 34.5372 },
      { latitude: 27.2579, longitude: 33.8116 },
    ]);
    expect(frameCamera(places, card(975)).zoom).toBeGreaterThanOrEqual(
      frameCamera(places, card(252)).zoom,
    );
  });

  it("fits a box that circles the world around its own point", () => {
    const fiji = placedLocations([
      {
        latitude: -17.7,
        longitude: 179,
        bbox_south: -21,
        bbox_north: -11,
        bbox_west: -180,
        bbox_east: 180,
      },
    ]);
    const { center, zoom } = frameCamera(fiji, card(252));
    expect(center.longitude).toBeCloseTo(179);
    expect(zoom).toBeGreaterThan(MIN_ZOOM);
  });
});

describe("worldCamera", () => {
  // A hero's frame, as the Home page's map is fitted for.
  const hero = (width: number): MapFrame => ({
    ...card(width, 352),
    inset: mapCanvas(width, true).inset,
  });
  const places = placedLocations([
    { latitude: 28.5, longitude: 34.5 }, // Dahab
    { latitude: 9.9, longitude: 123.4 }, // Moalboal
  ]);

  it("keeps Greenwich at the middle where every pin fits, at the picture's one zoom", () => {
    const camera = worldCamera(places, hero(1280));
    expect(camera.center.longitude).toBe(0);
    expect(camera.zoom).toBe(0);
    expect(camera.center.latitude).toBe(
      frameCamera(places, hero(1280)).center.latitude,
    );
  });

  it("moves across to the pins' middle where a narrow frame would leave one off it", () => {
    const frame = hero(390);
    const camera = worldCamera(places, frame);
    expect(camera.center.longitude).toBeCloseTo(78.95, 1);

    const layout = tileLayout(camera, whole(frame, { x: 195, y: 176 }));
    for (const place of places) {
      expect(Math.abs(projectFrom(layout, place).left)).toBeLessThanOrEqual(
        195 - frame.inset,
      );
    }
  });

  it("crosses the antimeridian to the pins' middle when that is their short way round", () => {
    const pacific = placedLocations([
      { latitude: -17.7, longitude: 178.1 }, // Fiji
      { latitude: -13.8, longitude: -172.1 }, // Samoa
    ]);
    expect(
      Math.abs(worldCamera(pacific, hero(390)).center.longitude),
    ).toBeGreaterThan(170);
  });

  it("keeps Greenwich where no way round fits every pin", () => {
    const everywhere = placedLocations([
      { latitude: 22.9, longitude: -109.9 }, // Cabo San Lucas
      { latitude: 28.5, longitude: 34.5 }, // Dahab
      { latitude: -8.35, longitude: 116.04 }, // Gili Trawangan
    ]);
    expect(worldCamera(everywhere, hero(390)).center.longitude).toBe(0);
  });

  it("shows nothing placed as the world from Greenwich", () => {
    expect(worldCamera([], hero(390)).center).toEqual({
      latitude: 0,
      longitude: 0,
    });
  });
});

describe("tileLayout", () => {
  it("floors the zoom to a whole one, within the zooms tiles are drawn at", () => {
    const area = { left: -100, top: -100, right: 100, bottom: 100 };
    const at = (zoom: number) =>
      tileLayout({ center: { latitude: 0, longitude: 0 }, zoom }, area).zoom;
    expect(at(6.9)).toBe(6);
    expect(at(9)).toBe(9);
    expect(at(-1.2)).toBe(MIN_ZOOM);
    expect(at(12)).toBe(MAX_FIT_ZOOM);
  });

  it("covers the whole area with tiles that meet, and asks for no more", () => {
    const frame = card(536);
    const anchor = { x: 268, y: 90 };
    const area = whole(frame, anchor);
    const { tiles } = tileLayout(frameCamera(BLUE_HOLE, frame), area);

    for (const { left, top } of tiles) {
      // Every tile reaches into the area.
      expect(left + TILE_SIZE).toBeGreaterThan(area.left);
      expect(left).toBeLessThan(area.right);
      expect(top + TILE_SIZE).toBeGreaterThan(area.top);
      expect(top).toBeLessThan(area.bottom);
    }
    // And every pixel of the area is under one of them.
    for (let x = area.left; x < area.right; x += 7) {
      for (let y = area.top; y < area.bottom; y += 7) {
        expect(
          tiles.some(
            ({ left, top }) =>
              x >= left &&
              x < left + TILE_SIZE &&
              y >= top &&
              y < top + TILE_SIZE,
          ),
        ).toBe(true);
      }
    }
    // A list card on a coast is at most three tiles by two.
    expect(tiles.length).toBeLessThanOrEqual(6);
  });

  it("names the tiles of one place at one size the same, whoever asks", () => {
    const frame = card(252);
    const area = whole(frame, { x: 126, y: 90 });
    const once = tileLayout(frameCamera(BLUE_HOLE, frame), area);
    const twice = tileLayout(frameCamera(BLUE_HOLE, frame), area);
    expect(twice).toEqual(once);
    expect(once.tiles.map(({ z, x, y }) => `${z}/${x}/${y}`)).toContain(
      "9/305/213",
    );
  });

  it("wraps a column past the antimeridian onto the grid", () => {
    // Fiji and Samoa, across the antimeridian.
    const places = placedLocations([
      { latitude: -18.1416, longitude: 178.4419 },
      { latitude: -13.8333, longitude: -171.7667 },
    ]);
    const frame = card(975);
    const layout = tileLayout(
      frameCamera(places, frame),
      whole(frame, { x: 488, y: 90 }),
    );
    const columns = new Set(layout.tiles.map(({ x }) => x));
    const last = 2 ** layout.zoom - 1;
    expect(columns).toContain(0);
    expect(columns).toContain(last);
    for (const { x } of layout.tiles) {
      expect(x).toBeGreaterThanOrEqual(0);
      expect(x).toBeLessThanOrEqual(last);
    }
  });

  it("asks for no row past the world's top or bottom edge", () => {
    // The whole world, under a frame taller than it is at zoom 0.
    const frame = card(1024, 700);
    const layout = tileLayout(
      frameCamera([], frame),
      whole(frame, { x: 512, y: 350 }),
    );
    expect(layout.zoom).toBe(0);
    expect(new Set(layout.tiles.map(({ y }) => y))).toEqual(new Set([0]));
    // The one tile of the world, once per copy across the frame.
    expect(new Set(layout.tiles.map(({ x }) => x))).toEqual(new Set([0]));
    expect(layout.tiles.length).toBeGreaterThan(1);
  });

  it("puts every tile on whole pixels", () => {
    const frame = card(333);
    const layout = tileLayout(
      frameCamera(BLUE_HOLE, frame),
      whole(frame, { x: 167, y: 90 }),
    );
    for (const { left, top } of layout.tiles) {
      expect(Number.isInteger(left)).toBe(true);
      expect(Number.isInteger(top)).toBe(true);
    }
  });
});

describe("projectFrom", () => {
  it("puts the camera's centre where the frame shows it", () => {
    const camera = {
      center: { latitude: 28.5721, longitude: 34.5372 },
      zoom: 9,
    };
    const layout = tileLayout(camera, {
      left: -10,
      top: -10,
      right: 10,
      bottom: 10,
    });
    const { left, top } = projectFrom(layout, camera.center);
    expect(Math.abs(left)).toBeLessThanOrEqual(0.5);
    expect(Math.abs(top)).toBeLessThanOrEqual(0.5);
  });

  it("keeps a place beside the antimeridian on the copy nearest the centre", () => {
    const layout = tileLayout(
      { center: { latitude: -16, longitude: -176.7 }, zoom: 4 },
      { left: -10, top: -10, right: 10, bottom: 10 },
    );
    const fiji = projectFrom(layout, { latitude: -18.14, longitude: 178.44 });
    const samoa = projectFrom(layout, { latitude: -13.83, longitude: -171.77 });
    expect(fiji.left).toBeLessThan(0);
    expect(samoa.left).toBeGreaterThan(0);
    // A few degrees apart, not a world.
    expect(samoa.left - fiji.left).toBeLessThan(TILE_SIZE);
  });
});

describe("tileCenter", () => {
  it("is the middle of the tile, which projects into it", () => {
    const middle = tileCenter(9, 305, 213);
    const layout = tileLayout(
      { center: middle, zoom: 9 },
      { left: -1, top: -1, right: 1, bottom: 1 },
    );
    expect(layout.tiles).toEqual([
      { z: 9, x: 305, y: 213, left: -256, top: -256 },
    ]);
  });

  it("is the world's middle at zoom 0", () => {
    const { latitude, longitude } = tileCenter(0, 0, 0);
    expect(latitude).toBeCloseTo(0, 9);
    expect(longitude).toBe(0);
  });
});