Keep several Ziele and fill the Cache from Einstellungen.
A Koordinate adds another place, and Hessen fetches only missing Kacheln without marking a Gebiet. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,6 +21,18 @@ function stringsFrom(value: unknown): string[] {
|
||||
return value.filter((item): item is string => typeof item === "string");
|
||||
}
|
||||
|
||||
function rememberedFrom(value: unknown): { heruntergeladen: string[]; ziele: string[] } {
|
||||
if (Array.isArray(value)) return { heruntergeladen: stringsFrom(value), ziele: [] };
|
||||
if (value && typeof value === "object") {
|
||||
const record = value as { heruntergeladen?: unknown; ziele?: unknown };
|
||||
return {
|
||||
heruntergeladen: stringsFrom(record.heruntergeladen),
|
||||
ziele: stringsFrom(record.ziele),
|
||||
};
|
||||
}
|
||||
return { heruntergeladen: [], ziele: [] };
|
||||
}
|
||||
|
||||
function settle(tx: IDBTransaction, value: () => unknown): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
tx.oncomplete = () => resolve(value());
|
||||
@@ -37,7 +49,7 @@ export function createMarksDb(): MarksPort {
|
||||
const tx = db.transaction(STORE, "readonly");
|
||||
const request = tx.objectStore(STORE).get(KEY);
|
||||
const value = await settle(tx, () => request.result);
|
||||
return { heruntergeladen: stringsFrom(value) };
|
||||
return rememberedFrom(value);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
@@ -47,7 +59,10 @@ export function createMarksDb(): MarksPort {
|
||||
const db = await openDatabase();
|
||||
try {
|
||||
const tx = db.transaction(STORE, "readwrite");
|
||||
tx.objectStore(STORE).put(state.heruntergeladen, KEY);
|
||||
tx.objectStore(STORE).put(
|
||||
{ heruntergeladen: state.heruntergeladen, ziele: state.ziele },
|
||||
KEY,
|
||||
);
|
||||
await settle(tx, () => undefined);
|
||||
} finally {
|
||||
db.close();
|
||||
|
||||
+3
-1
@@ -17,12 +17,14 @@ export function createMemoryCache(initial: string[] = []): CachePort {
|
||||
|
||||
export function createMemoryMarks(initial: string[] = []): MarksPort {
|
||||
let heruntergeladen = [...initial];
|
||||
let ziele: string[] = [];
|
||||
return {
|
||||
async read() {
|
||||
return { heruntergeladen: [...heruntergeladen] };
|
||||
return { heruntergeladen: [...heruntergeladen], ziele: [...ziele] };
|
||||
},
|
||||
async write(state) {
|
||||
heruntergeladen = [...state.heruntergeladen];
|
||||
ziele = [...state.ziele];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,13 +17,14 @@ const GITTER_PATTERN = /^(\d{1,2})([C-HJ-NP-X])([A-HJ-NP-Z]{2})$/;
|
||||
export function parseKoordinate(
|
||||
input: { gitter: string; rechtswert: string; hochwert: string },
|
||||
blattCovers: (point: Point) => boolean,
|
||||
): { ok: true; point: Point } | { ok: false; notice: string } {
|
||||
): { ok: true; point: Point; koordinate: string } | { ok: false; notice: string } {
|
||||
if (!/^\d{5}$/.test(input.rechtswert) || !/^\d{5}$/.test(input.hochwert)) {
|
||||
return { ok: false, notice: DIGITS };
|
||||
}
|
||||
|
||||
const compact = input.gitter.trim().toUpperCase().replace(/\s+/g, "");
|
||||
if (!GITTER_PATTERN.test(compact)) {
|
||||
const gitter = GITTER_PATTERN.exec(compact);
|
||||
if (!gitter) {
|
||||
return { ok: false, notice: GITTER_NOTICE };
|
||||
}
|
||||
|
||||
@@ -37,5 +38,10 @@ export function parseKoordinate(
|
||||
const projected = proj4("EPSG:4326", "EPSG:25832", geographic);
|
||||
const point = { easting: projected[0], northing: projected[1] };
|
||||
if (!blattCovers(point)) return { ok: false, notice: BLATT };
|
||||
return { ok: true, point };
|
||||
const canonicalGitter = `${gitter[1]}${gitter[2]} ${gitter[3]}`;
|
||||
return {
|
||||
ok: true,
|
||||
point,
|
||||
koordinate: `${canonicalGitter} ${input.rechtswert} ${input.hochwert}`,
|
||||
};
|
||||
}
|
||||
|
||||
+337
-179
@@ -1,7 +1,7 @@
|
||||
import { shippedConfig } from "../config";
|
||||
import { createFetchClock, createMemoryCache, createMemoryMarks } from "./fakes";
|
||||
import { openKarte } from "./session";
|
||||
import type { KarteSession, KarteSnapshot, Point, Uebersicht, Viewport } from "./types";
|
||||
import type { KarteSession, Point, Uebersicht, Viewport } from "./types";
|
||||
|
||||
const sheet: Uebersicht = {
|
||||
tileSizePx: 10,
|
||||
@@ -71,12 +71,6 @@ function appearance(karte: KarteSession, id: string) {
|
||||
return kachel.appearance;
|
||||
}
|
||||
|
||||
function hessen(snapshot: KarteSnapshot) {
|
||||
const gebiet = snapshot.gebiete.find((item) => item.name === "Hessen");
|
||||
if (!gebiet) throw new Error("Hessen missing");
|
||||
return gebiet;
|
||||
}
|
||||
|
||||
const sample = { gitter: "32U MA", rechtswert: "67030", hochwert: "78109" };
|
||||
|
||||
describe("opening the Karte", () => {
|
||||
@@ -87,7 +81,7 @@ describe("opening the Karte", () => {
|
||||
expect(snapshot.resolution).toBe(7);
|
||||
expect(snapshot.center).toEqual({ easting: 150, northing: 50 });
|
||||
expect(snapshot.koordinate.gitter).toBe("32U MA");
|
||||
expect(snapshot.ziel).toBeNull();
|
||||
expect(snapshot.ziele).toEqual([]);
|
||||
expect(snapshot.standort).toBeNull();
|
||||
});
|
||||
|
||||
@@ -243,18 +237,6 @@ describe("fetching a missing Kachel", () => {
|
||||
expect(appearance(opened.karte, "1/0/0")).toBe("blank-downloading");
|
||||
});
|
||||
|
||||
test("leaving the app pauses the fetch, and opening online continues it", async () => {
|
||||
const { karte, clock } = await viewTile(250);
|
||||
karte.setOnline(true);
|
||||
karte.startGebiet("Hessen");
|
||||
karte.setAppOpen(false);
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_C)).toBe("blank-not-on-device");
|
||||
karte.setAppOpen(true);
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_C)).toBe("drawn");
|
||||
});
|
||||
|
||||
test("a finished Kachel is still drawn after the app is left and opened again", async () => {
|
||||
const cache = createMemoryCache();
|
||||
const marks = createMemoryMarks();
|
||||
@@ -270,7 +252,7 @@ describe("fetching a missing Kachel", () => {
|
||||
second.karte.zoomTo(10);
|
||||
second.karte.panTo({ easting: 250, northing: 50 });
|
||||
expect(appearance(second.karte, TILE_C)).toBe("drawn");
|
||||
expect(second.karte.snapshot().ziel).toBeNull();
|
||||
expect(second.karte.snapshot().ziele).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -281,24 +263,29 @@ describe("naming a Ziel", () => {
|
||||
const snapshot = karte.snapshot();
|
||||
expect(snapshot.koordinate).toEqual({ gitter: "32u ma", rechtswert: "67030", hochwert: "78109" });
|
||||
expect(snapshot.resolution).toBe(7);
|
||||
expect(snapshot.ziel).not.toBeNull();
|
||||
expect(Math.abs(snapshot.ziel!.easting - 467_030)).toBeLessThan(1);
|
||||
expect(Math.abs(snapshot.ziel!.northing - 5_578_109)).toBeLessThan(1);
|
||||
expect(snapshot.ziele).toHaveLength(1);
|
||||
expect(snapshot.ziele[0].koordinate).toBe("32U MA 67030 78109");
|
||||
expect(Math.abs(snapshot.ziele[0].point.easting - 467_030)).toBeLessThan(1);
|
||||
expect(Math.abs(snapshot.ziele[0].point.northing - 5_578_109)).toBeLessThan(1);
|
||||
expect(snapshot.ziele[0].entfernung).toBeNull();
|
||||
expect(snapshot.center).toEqual(snapshot.ziele[0].point);
|
||||
expect(snapshot.notice).toBe("Nicht auf dem Gerät.");
|
||||
});
|
||||
|
||||
test("treats 32UMA like 32U MA", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: wideSquare });
|
||||
karte.enterKoordinate({ gitter: "32UMA", rechtswert: "67030", hochwert: "78109" });
|
||||
expect(Math.abs(karte.snapshot().ziel!.easting - 467_030)).toBeLessThan(1);
|
||||
expect(karte.snapshot().koordinate.gitter).toBe("32UMA");
|
||||
const snapshot = karte.snapshot();
|
||||
expect(snapshot.ziele[0].koordinate).toBe("32U MA 67030 78109");
|
||||
expect(Math.abs(snapshot.ziele[0].point.easting - 467_030)).toBeLessThan(1);
|
||||
expect(snapshot.koordinate.gitter).toBe("32UMA");
|
||||
});
|
||||
|
||||
test("accepts a leading zero as five digits, and works with the device offline", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: wideSquare, online: false });
|
||||
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "06703", hochwert: "78109" });
|
||||
expect(karte.snapshot().ziel).not.toBeNull();
|
||||
expect(Math.abs(karte.snapshot().ziel!.easting - 406_703)).toBeLessThan(1);
|
||||
expect(karte.snapshot().ziele[0].koordinate).toBe("32U MA 06703 78109");
|
||||
expect(Math.abs(karte.snapshot().ziele[0].point.easting - 406_703)).toBeLessThan(1);
|
||||
});
|
||||
|
||||
test("rejects a Rechtswert or Hochwert that is not exactly five digits", async () => {
|
||||
@@ -308,13 +295,14 @@ describe("naming a Ziel", () => {
|
||||
karte.panTo({ easting: 420_000, northing: 5_520_000 });
|
||||
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "6703", hochwert: "78109" });
|
||||
expect(karte.snapshot().notice).toBe("Rechtswert und Hochwert müssen genau fünf Ziffern haben.");
|
||||
expect(karte.snapshot().ziel).toEqual(kept.ziel);
|
||||
expect(karte.snapshot().ziele).toEqual(kept.ziele);
|
||||
expect(karte.snapshot().center).toEqual({ easting: 420_000, northing: 5_520_000 });
|
||||
expect(karte.snapshot().koordinate).toEqual(kept.koordinate);
|
||||
|
||||
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "670300", hochwert: "78109" });
|
||||
expect(karte.snapshot().notice).toBe("Rechtswert und Hochwert müssen genau fünf Ziffern haben.");
|
||||
expect(karte.snapshot().ziel).toEqual(kept.ziel);
|
||||
expect(karte.snapshot().ziele).toEqual(kept.ziele);
|
||||
expect(karte.snapshot().koordinate).toEqual(kept.koordinate);
|
||||
});
|
||||
|
||||
test("rejects an impossible Gitter", async () => {
|
||||
@@ -323,31 +311,62 @@ describe("naming a Ziel", () => {
|
||||
const kept = karte.snapshot();
|
||||
karte.enterKoordinate({ gitter: "32U II", rechtswert: "67030", hochwert: "78109" });
|
||||
expect(karte.snapshot().notice).toBe("Dieses Gitter ist keine gültige Koordinate.");
|
||||
expect(karte.snapshot().ziel).toEqual(kept.ziel);
|
||||
expect(karte.snapshot().ziele).toEqual(kept.ziele);
|
||||
expect(karte.snapshot().center).toEqual(kept.center);
|
||||
expect(karte.snapshot().koordinate).toEqual(kept.koordinate);
|
||||
});
|
||||
|
||||
test("rejects a Koordinate no Blatt covers, including a hole between Blätter", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: withHole });
|
||||
karte.enterKoordinate(sample);
|
||||
const kept = karte.snapshot();
|
||||
expect(kept.ziel).not.toBeNull();
|
||||
expect(kept.ziele).toHaveLength(1);
|
||||
|
||||
karte.panTo({ easting: 465_000, northing: 5_580_000 });
|
||||
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "75000", hochwert: "78109" });
|
||||
expect(karte.snapshot().notice).toBe("Kein Blatt deckt diese Koordinate.");
|
||||
expect(karte.snapshot().ziel).toEqual(kept.ziel);
|
||||
expect(karte.snapshot().ziele).toEqual(kept.ziele);
|
||||
expect(karte.snapshot().center).toEqual({ easting: 465_000, northing: 5_580_000 });
|
||||
expect(karte.snapshot().resolution).toBe(kept.resolution);
|
||||
expect(karte.snapshot().koordinate).toEqual(kept.koordinate);
|
||||
});
|
||||
|
||||
test("a second Koordinate replaces the Ziel, and there is only one", async () => {
|
||||
test("a valid Koordinate clears the previous refusal", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: wideSquare, online: true });
|
||||
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "6703", hochwert: "78109" });
|
||||
expect(karte.snapshot().notice).toBe("Rechtswert und Hochwert müssen genau fünf Ziffern haben.");
|
||||
karte.enterKoordinate(sample);
|
||||
expect(karte.snapshot().notice).toBeNull();
|
||||
expect(karte.snapshot().ziele).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("a repeated Koordinate is refused and changes nothing", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: wideSquare });
|
||||
karte.enterKoordinate({ gitter: "32u ma", rechtswert: "67030", hochwert: "78109" });
|
||||
const kept = karte.snapshot();
|
||||
karte.panTo({ easting: 420_000, northing: 5_520_000 });
|
||||
karte.zoomTo(10);
|
||||
karte.enterKoordinate({ gitter: "32UMA", rechtswert: "67030", hochwert: "78109" });
|
||||
const snapshot = karte.snapshot();
|
||||
expect(snapshot.notice).toBe("Diese Koordinate ist schon ein Ziel.");
|
||||
expect(snapshot.ziele).toEqual(kept.ziele);
|
||||
expect(snapshot.center).toEqual({ easting: 420_000, northing: 5_520_000 });
|
||||
expect(snapshot.resolution).toBe(10);
|
||||
expect(snapshot.koordinate).toEqual(kept.koordinate);
|
||||
});
|
||||
|
||||
test("a second Koordinate adds another Ziel, newest first", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: withHole });
|
||||
karte.enterKoordinate(sample);
|
||||
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "81000", hochwert: "78109" });
|
||||
const snapshot = karte.snapshot();
|
||||
expect(Math.abs(snapshot.ziel!.easting - 481_000)).toBeLessThan(1);
|
||||
expect(snapshot.ziele.map((ziel) => ziel.koordinate)).toEqual([
|
||||
"32U MA 81000 78109",
|
||||
"32U MA 67030 78109",
|
||||
]);
|
||||
expect(Math.abs(snapshot.ziele[0].point.easting - 481_000)).toBeLessThan(1);
|
||||
expect(snapshot.resolution).toBe(7);
|
||||
expect(snapshot.center).toEqual(snapshot.ziele[0].point);
|
||||
expect(snapshot.koordinate).toEqual({
|
||||
gitter: "32U MA",
|
||||
rechtswert: "81000",
|
||||
@@ -355,21 +374,64 @@ describe("naming a Ziel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("leaving the page drops the Ziel and keeps the Cache marks", async () => {
|
||||
test("deleteZiel removes that Ziel and leaves the view", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: withHole });
|
||||
karte.enterKoordinate(sample);
|
||||
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "81000", hochwert: "78109" });
|
||||
karte.panTo({ easting: 465_000, northing: 5_580_000 });
|
||||
karte.zoomTo(10);
|
||||
karte.deleteZiel("32U MA 67030 78109");
|
||||
const snapshot = karte.snapshot();
|
||||
expect(snapshot.ziele.map((ziel) => ziel.koordinate)).toEqual(["32U MA 81000 78109"]);
|
||||
expect(snapshot.center).toEqual({ easting: 465_000, northing: 5_580_000 });
|
||||
expect(snapshot.resolution).toBe(10);
|
||||
});
|
||||
|
||||
test("leaving the page keeps the Ziele and the Cache marks", async () => {
|
||||
const cache = createMemoryCache();
|
||||
const marks = createMemoryMarks();
|
||||
const first = await openSheet({ uebersicht: wideSquare, cache, marks });
|
||||
first.karte.enterKoordinate({ gitter: "32u ma", rechtswert: "67030", hochwert: "78109" });
|
||||
first.karte.enterKoordinate({ gitter: "32UMA", rechtswert: "81000", hochwert: "78109" });
|
||||
first.karte.reportStandort({ easting: 420_000, northing: 5_520_000 });
|
||||
expect(first.karte.snapshot().ziele.map((ziel) => ziel.koordinate)).toEqual([
|
||||
"32U MA 81000 78109",
|
||||
"32U MA 67030 78109",
|
||||
]);
|
||||
|
||||
const kept = await openSheet({ uebersicht: wideSquare, cache, marks });
|
||||
expect(kept.karte.snapshot().ziele.map((ziel) => ziel.koordinate)).toEqual([
|
||||
"32U MA 81000 78109",
|
||||
"32U MA 67030 78109",
|
||||
]);
|
||||
expect(Math.abs(kept.karte.snapshot().ziele[1].point.easting - 467_030)).toBeLessThan(1);
|
||||
expect(kept.karte.snapshot().ziele[0].entfernung).toBeNull();
|
||||
expect(kept.karte.snapshot().standort).toBeNull();
|
||||
expect(kept.karte.snapshot().koordinate).toEqual({
|
||||
gitter: "32U MA",
|
||||
rechtswert: "",
|
||||
hochwert: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("deleting the Cache leaves the Ziele in place", async () => {
|
||||
const cache = createMemoryCache();
|
||||
const marks = createMemoryMarks();
|
||||
const first = await openSheet({ uebersicht: wideSquare, cache, marks });
|
||||
first.karte.enterKoordinate(sample);
|
||||
first.karte.startGebiet("Hessen");
|
||||
expect(first.karte.snapshot().ziel).not.toBeNull();
|
||||
first.karte.deleteCache();
|
||||
expect(first.karte.snapshot().ziele.map((ziel) => ziel.koordinate)).toEqual([
|
||||
"32U MA 67030 78109",
|
||||
]);
|
||||
|
||||
const second = await openSheet({ uebersicht: wideSquare, cache, marks });
|
||||
expect(second.karte.snapshot().ziel).toBeNull();
|
||||
expect(hessen(second.karte.snapshot()).heruntergeladen).toBe(true);
|
||||
expect(second.karte.snapshot().ziele.map((ziel) => ziel.koordinate)).toEqual([
|
||||
"32U MA 67030 78109",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Standort and distance", () => {
|
||||
describe("Standort and Entfernung", () => {
|
||||
test("a late first Standort centers the view once", async () => {
|
||||
const { karte } = await openSheet();
|
||||
expect(karte.snapshot().center).toEqual({ easting: 150, northing: 50 });
|
||||
@@ -424,24 +486,26 @@ describe("Standort and distance", () => {
|
||||
expect(karte.snapshot().center).toEqual({ easting: 5, northing: 5 });
|
||||
});
|
||||
|
||||
test("centers on the Standort and on the Ziel at 1:25,000", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: wideSquare });
|
||||
test("centers on the Standort and on one Ziel at 1:25,000", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: withHole });
|
||||
const before = karte.snapshot();
|
||||
karte.centerOnStandort();
|
||||
karte.centerOnZiel();
|
||||
karte.centerOnZiel("32U MA 67030 78109");
|
||||
expect(karte.snapshot().center).toEqual(before.center);
|
||||
expect(karte.snapshot().resolution).toBe(before.resolution);
|
||||
|
||||
karte.enterKoordinate(sample);
|
||||
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "81000", hochwert: "78109" });
|
||||
const older = karte.snapshot().ziele[1].point;
|
||||
karte.reportStandort({
|
||||
easting: karte.snapshot().ziel!.easting + 100,
|
||||
northing: karte.snapshot().ziel!.northing,
|
||||
easting: older.easting + 100,
|
||||
northing: older.northing,
|
||||
});
|
||||
karte.zoomTo(1.25);
|
||||
karte.panTo({ easting: 410_000, northing: 5_510_000 });
|
||||
karte.centerOnZiel();
|
||||
karte.panTo({ easting: 465_000, northing: 5_580_000 });
|
||||
karte.centerOnZiel("32U MA 67030 78109");
|
||||
expect(karte.snapshot().resolution).toBe(7);
|
||||
expect(karte.snapshot().center).toEqual(karte.snapshot().ziel);
|
||||
expect(karte.snapshot().center).toEqual(older);
|
||||
|
||||
karte.zoomTo(1.25);
|
||||
karte.centerOnStandort();
|
||||
@@ -449,32 +513,64 @@ describe("Standort and distance", () => {
|
||||
expect(karte.snapshot().center).toEqual(karte.snapshot().standort);
|
||||
});
|
||||
|
||||
test("shows metres below 1000 m and kilometres from 1000 m, and hides a missing end", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: wideSquare, online: false });
|
||||
expect(karte.snapshot().distance).toBeNull();
|
||||
test("shows metres below 1000 m and kilometres from 1000 m, and hides a missing Standort", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: withHole, online: false });
|
||||
karte.enterKoordinate(sample);
|
||||
expect(karte.snapshot().distance).toBeNull();
|
||||
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "81000", hochwert: "78109" });
|
||||
expect(karte.snapshot().ziele.every((ziel) => ziel.entfernung === null)).toBe(true);
|
||||
|
||||
const ziel = karte.snapshot().ziel!;
|
||||
karte.reportStandort({ easting: ziel.easting, northing: ziel.northing + 500 });
|
||||
expect(karte.snapshot().distance).toEqual({ metres: 500, text: "500 m" });
|
||||
const nearer = karte.snapshot().ziele[0].point;
|
||||
const keptCenter = karte.snapshot().center;
|
||||
karte.reportStandort({ easting: nearer.easting, northing: nearer.northing + 500 });
|
||||
expect(karte.snapshot().ziele[0].entfernung).toEqual({ metres: 500, text: "500 m" });
|
||||
expect(karte.snapshot().ziele[1].entfernung?.text).not.toBe("500 m");
|
||||
expect(karte.snapshot().center).toEqual(keptCenter);
|
||||
|
||||
karte.reportStandort({ easting: ziel.easting, northing: ziel.northing + 999 });
|
||||
expect(karte.snapshot().distance?.text).toBe("999 m");
|
||||
karte.reportStandort({ easting: nearer.easting, northing: nearer.northing + 999 });
|
||||
expect(karte.snapshot().ziele[0].entfernung?.text).toBe("999 m");
|
||||
|
||||
karte.reportStandort({ easting: ziel.easting, northing: ziel.northing + 1000 });
|
||||
expect(karte.snapshot().distance?.text).toBe("1 km");
|
||||
karte.reportStandort({ easting: nearer.easting, northing: nearer.northing + 1000 });
|
||||
expect(karte.snapshot().ziele[0].entfernung?.text).toBe("1 km");
|
||||
|
||||
karte.reportStandort({ easting: ziel.easting, northing: ziel.northing + 1500 });
|
||||
expect(karte.snapshot().distance?.text).toBe("1,5 km");
|
||||
karte.reportStandort({ easting: nearer.easting, northing: nearer.northing + 1500 });
|
||||
expect(karte.snapshot().ziele[0].entfernung?.text).toBe("1,5 km");
|
||||
|
||||
karte.reportStandort({ easting: ziel.easting + 3000, northing: ziel.northing + 4000 });
|
||||
expect(karte.snapshot().distance).toEqual({ metres: 5000, text: "5 km" });
|
||||
expect(karte.snapshot().center).toEqual(ziel);
|
||||
karte.reportStandort({ easting: nearer.easting + 3000, northing: nearer.northing + 4000 });
|
||||
expect(karte.snapshot().ziele[0].entfernung).toEqual({ metres: 5000, text: "5 km" });
|
||||
expect(karte.snapshot().ziele[1].entfernung?.metres).not.toBe(5000);
|
||||
expect(karte.snapshot().center).toEqual(keptCenter);
|
||||
});
|
||||
});
|
||||
|
||||
describe("downloading and removing Hessen", () => {
|
||||
describe("downloading Hessen", () => {
|
||||
test("reports how many Hessen Kacheln are already in the Cache", async () => {
|
||||
const { karte } = await openSheet();
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 0, total: 3 });
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
});
|
||||
|
||||
test("a stored heruntergeladen name does not fetch Hessen", async () => {
|
||||
const marks = createMemoryMarks(["Hessen"]);
|
||||
const { karte, clock } = await openSheet({ viewport: oneTile, marks });
|
||||
karte.zoomTo(10);
|
||||
karte.panTo({ easting: 250, northing: 50 });
|
||||
karte.setOnline(true);
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
karte.setOnline(false);
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-not-on-device");
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 1, total: 3 });
|
||||
});
|
||||
|
||||
test("a Kachel already in the Cache counts as arrived Hessen", async () => {
|
||||
const { karte } = await openSheet({ cache: createMemoryCache([TILE_A]) });
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 1, total: 3 });
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
});
|
||||
|
||||
async function view(easting: number, viewport: Viewport = oneTile) {
|
||||
const opened = await openSheet({ viewport });
|
||||
opened.karte.zoomTo(10);
|
||||
@@ -483,141 +579,203 @@ describe("downloading and removing Hessen", () => {
|
||||
return opened;
|
||||
}
|
||||
|
||||
test("starting Hessen marks it heruntergeladen and fetches the current view first", async () => {
|
||||
test("starting Hessen fetches only the missing Kacheln, current view first", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
karte.startGebiet("Hessen");
|
||||
const started = hessen(karte.snapshot());
|
||||
expect(started.heruntergeladen).toBe(true);
|
||||
expect(started.arrived).toBe(0);
|
||||
expect(started.total).toBe(3);
|
||||
karte.startHessen();
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 0, total: 3 });
|
||||
expect(karte.snapshot().visit).toEqual({ arrived: 0, total: 3 });
|
||||
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_C)).toBe("drawn");
|
||||
expect(hessen(karte.snapshot()).arrived).toBe(1);
|
||||
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 1, total: 3 });
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).not.toBe("drawn");
|
||||
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||
expect(hessen(karte.snapshot()).arrived).toBe(3);
|
||||
});
|
||||
|
||||
test("Kacheln already in the Cache stay usable while Hessen is still downloading", async () => {
|
||||
const { karte } = await openSheet({
|
||||
test("a finished Hessen fill leaves no visit progress", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
karte.startHessen();
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 3, total: 3 });
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||
karte.panTo({ easting: 150, northing: 50 });
|
||||
expect(appearance(karte, TILE_B)).toBe("drawn");
|
||||
});
|
||||
|
||||
test("a second Hessen start while that fill is unfinished adds nothing", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
karte.startHessen();
|
||||
await clock.tick();
|
||||
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||
karte.startHessen();
|
||||
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 3, total: 3 });
|
||||
});
|
||||
|
||||
test("closing the app stops the Hessen fill and opening does not continue it", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
karte.startHessen();
|
||||
karte.setAppOpen(false);
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_C)).toBe("blank-not-on-device");
|
||||
|
||||
karte.setAppOpen(true);
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_C)).toBe("drawn");
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
karte.setOnline(false);
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-not-on-device");
|
||||
|
||||
karte.setOnline(true);
|
||||
karte.startHessen();
|
||||
expect(karte.snapshot().visit).toEqual({ arrived: 0, total: 2 });
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||
await clock.tick();
|
||||
karte.panTo({ easting: 150, northing: 50 });
|
||||
expect(appearance(karte, TILE_B)).toBe("drawn");
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
});
|
||||
|
||||
test("looking around fetches only the missing Kacheln of the current view", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 1, total: 3 });
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-downloading");
|
||||
});
|
||||
|
||||
test("losing the network pauses the Hessen fill and regaining it continues", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
karte.startHessen();
|
||||
await clock.tick();
|
||||
karte.setOnline(false);
|
||||
expect(appearance(karte, TILE_C)).toBe("drawn");
|
||||
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-not-on-device");
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-not-on-device");
|
||||
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||
karte.setOnline(true);
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-downloading");
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||
expect(karte.snapshot().visit).toEqual({ arrived: 2, total: 3 });
|
||||
});
|
||||
|
||||
test("starting Hessen fetches every prepared scale, the current scale first", async () => {
|
||||
const opened = await openSheet({
|
||||
viewport: oneTile,
|
||||
uebersicht: {
|
||||
tileSizePx: 10,
|
||||
resolutions: [10, 20],
|
||||
blaetter: [{ minE: 0, minN: 0, maxE: 100, maxN: 100 }],
|
||||
gebiete: [{ name: "Hessen", covers: "all" }],
|
||||
},
|
||||
});
|
||||
opened.karte.zoomTo(10);
|
||||
opened.karte.panTo({ easting: 50, northing: 50 });
|
||||
opened.karte.setOnline(true);
|
||||
expect(opened.karte.snapshot().hessen).toEqual({ arrived: 0, total: 2 });
|
||||
opened.karte.startHessen();
|
||||
expect(opened.karte.snapshot().visit).toEqual({ arrived: 0, total: 2 });
|
||||
await opened.clock.tick();
|
||||
expect(appearance(opened.karte, "0/0/0")).toBe("drawn");
|
||||
opened.karte.zoomTo(20);
|
||||
expect(appearance(opened.karte, "1/0/0")).not.toBe("drawn");
|
||||
await opened.clock.tick();
|
||||
expect(appearance(opened.karte, "1/0/0")).toBe("drawn");
|
||||
expect(opened.karte.snapshot().visit).toBeNull();
|
||||
expect(opened.karte.snapshot().hessen).toEqual({ arrived: 2, total: 2 });
|
||||
});
|
||||
|
||||
test("looking around fetches the current view before the rest of Hessen", async () => {
|
||||
const opened = await openSheet({
|
||||
viewport: oneTile,
|
||||
uebersicht: {
|
||||
tileSizePx: 10,
|
||||
resolutions: [10, 20],
|
||||
blaetter: [{ minE: 0, minN: 0, maxE: 300, maxN: 100 }],
|
||||
gebiete: [{ name: "Hessen", covers: "all" }],
|
||||
},
|
||||
});
|
||||
opened.karte.zoomTo(10);
|
||||
opened.karte.panTo({ easting: 50, northing: 50 });
|
||||
opened.karte.setOnline(true);
|
||||
opened.karte.startHessen();
|
||||
opened.karte.panTo({ easting: 250, northing: 50 });
|
||||
await opened.clock.tick();
|
||||
await opened.clock.tick();
|
||||
expect(appearance(opened.karte, "0/2/0")).toBe("drawn");
|
||||
opened.karte.setOnline(false);
|
||||
opened.karte.zoomTo(20);
|
||||
expect(appearance(opened.karte, "1/1/0")).toBe("blank-not-on-device");
|
||||
opened.karte.zoomTo(10);
|
||||
opened.karte.panTo({ easting: 150, northing: 50 });
|
||||
expect(appearance(opened.karte, "0/1/0")).toBe("blank-not-on-device");
|
||||
});
|
||||
|
||||
test("Hessen skips Kacheln already in the Cache and keeps them drawn", async () => {
|
||||
const { karte, clock } = await openSheet({
|
||||
viewport: threeTiles,
|
||||
cache: createMemoryCache([TILE_A]),
|
||||
online: true,
|
||||
});
|
||||
karte.zoomTo(10);
|
||||
karte.panTo({ easting: 150, northing: 50 });
|
||||
karte.startGebiet("Hessen");
|
||||
karte.setOnline(true);
|
||||
karte.startHessen();
|
||||
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||
expect(appearance(karte, TILE_B)).toBe("blank-downloading");
|
||||
expect(hessen(karte.snapshot()).arrived).toBe(1);
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 1, total: 3 });
|
||||
expect(karte.snapshot().visit).toEqual({ arrived: 0, total: 2 });
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 3, total: 3 });
|
||||
});
|
||||
|
||||
test("a dropped connection keeps finished Kacheln and pauses the rest", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
karte.startGebiet("Hessen");
|
||||
await clock.tick();
|
||||
karte.setOnline(false);
|
||||
expect(appearance(karte, TILE_C)).toBe("drawn");
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-not-on-device");
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-not-on-device");
|
||||
karte.setOnline(true);
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-downloading");
|
||||
});
|
||||
|
||||
test("while Hessen is not heruntergeladen, looking around fetches only the current view", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-downloading");
|
||||
expect(hessen(karte.snapshot()).heruntergeladen).toBe(false);
|
||||
});
|
||||
|
||||
test("removing Hessen stops the rest and keeps a Kachel that still lies in Usingen", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
await clock.tick();
|
||||
karte.startGebiet("Usingen");
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
karte.startGebiet("Hessen");
|
||||
await clock.tick();
|
||||
karte.setOnline(false);
|
||||
karte.removeGebiet("Hessen");
|
||||
|
||||
expect(hessen(karte.snapshot()).heruntergeladen).toBe(false);
|
||||
expect(karte.snapshot().gebiete.find((gebiet) => gebiet.name === "Usingen")!.heruntergeladen).toBe(
|
||||
true,
|
||||
);
|
||||
expect(appearance(karte, TILE_C)).toBe("blank-not-on-device");
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||
karte.panTo({ easting: 150, northing: 50 });
|
||||
expect(appearance(karte, TILE_B)).toBe("drawn");
|
||||
|
||||
karte.setOnline(true);
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
karte.panTo({ easting: 250, northing: 50 });
|
||||
expect(appearance(karte, TILE_C)).not.toBe("drawn");
|
||||
});
|
||||
|
||||
test("removing Wehrheim keeps the overlap and drops the Kachel only Wehrheim held", async () => {
|
||||
const { karte, clock } = await openSheet({ viewport: threeTiles, online: true });
|
||||
karte.zoomTo(10);
|
||||
karte.panTo({ easting: 150, northing: 50 });
|
||||
karte.startGebiet("Usingen");
|
||||
karte.startGebiet("Wehrheim");
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
karte.setOnline(false);
|
||||
karte.removeGebiet("Wehrheim");
|
||||
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||
expect(appearance(karte, TILE_B)).toBe("drawn");
|
||||
expect(appearance(karte, TILE_C)).toBe("blank-not-on-device");
|
||||
expect(karte.snapshot().gebiete.find((gebiet) => gebiet.name === "Wehrheim")!.heruntergeladen).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("deleting the whole Cache clears marks and does not start Hessen again", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
karte.startGebiet("Hessen");
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
await clock.tick();
|
||||
test("deleting the Cache leaves the Ziel", async () => {
|
||||
const { karte } = await openSheet({ uebersicht: wideSquare, online: true });
|
||||
karte.enterKoordinate(sample);
|
||||
const ziele = karte.snapshot().ziele;
|
||||
karte.deleteCache();
|
||||
expect(hessen(karte.snapshot()).heruntergeladen).toBe(false);
|
||||
expect(hessen(karte.snapshot()).arrived).toBe(0);
|
||||
expect(karte.snapshot().ziele).toEqual(ziele);
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
});
|
||||
|
||||
test("deleting the Cache removes every Kachel and does not start a fetch", async () => {
|
||||
const { karte, clock } = await view(250);
|
||||
karte.startHessen();
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_C)).toBe("drawn");
|
||||
karte.deleteCache();
|
||||
expect(karte.snapshot().visit).toBeNull();
|
||||
expect(karte.snapshot().hessen).toEqual({ arrived: 0, total: 3 });
|
||||
expect(appearance(karte, TILE_C)).toBe("blank-not-on-device");
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_C)).toBe("blank-not-on-device");
|
||||
karte.panTo({ easting: 260, northing: 50 });
|
||||
expect(appearance(karte, TILE_C)).toBe("blank-downloading");
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_C)).toBe("drawn");
|
||||
expect(hessen(karte.snapshot()).heruntergeladen).toBe(false);
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).not.toBe("drawn");
|
||||
});
|
||||
|
||||
test("Hessen can be started again after it is removed", async () => {
|
||||
const { karte, clock } = await view(50);
|
||||
karte.startGebiet("Hessen");
|
||||
await clock.tick();
|
||||
karte.setOnline(false);
|
||||
karte.removeGebiet("Hessen");
|
||||
karte.panTo({ easting: 50, northing: 50 });
|
||||
expect(appearance(karte, TILE_A)).toBe("blank-not-on-device");
|
||||
karte.setOnline(true);
|
||||
karte.startGebiet("Hessen");
|
||||
expect(hessen(karte.snapshot()).heruntergeladen).toBe(true);
|
||||
await clock.tick();
|
||||
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||
});
|
||||
});
|
||||
|
||||
+106
-100
@@ -1,7 +1,6 @@
|
||||
import { maxResolutionMeters } from "../config";
|
||||
import { parseKoordinate } from "./koordinate";
|
||||
import type {
|
||||
GebietStatus,
|
||||
KachelView,
|
||||
KarteOptions,
|
||||
KarteSession,
|
||||
@@ -125,6 +124,15 @@ function formatDistance(metres: number): string {
|
||||
return `${kilometres.toFixed(1).replace(".", ",")} km`;
|
||||
}
|
||||
|
||||
function entfernungTo(
|
||||
standort: Point | null,
|
||||
point: Point,
|
||||
): { metres: number; text: string } | null {
|
||||
if (!standort) return null;
|
||||
const metres = Math.hypot(point.easting - standort.easting, point.northing - standort.northing);
|
||||
return { metres, text: formatDistance(metres) };
|
||||
}
|
||||
|
||||
function isAbort(error: unknown): boolean {
|
||||
return error instanceof DOMException && error.name === "AbortError";
|
||||
}
|
||||
@@ -140,17 +148,16 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
const stored = new Set(loaded);
|
||||
const cached = new Set(loaded.filter((id) => catalog.has(id)));
|
||||
const markState = await marks.read();
|
||||
const heruntergeladen = new Set(
|
||||
markState.heruntergeladen.filter((name) =>
|
||||
uebersicht.gebiete.some((gebiet) => gebiet.name === name),
|
||||
),
|
||||
);
|
||||
|
||||
const totals = new Map(uebersicht.gebiete.map((gebiet) => [gebiet.name, 0]));
|
||||
for (const entry of catalog.values()) {
|
||||
for (const name of entry.gebiete) {
|
||||
totals.set(name, (totals.get(name) ?? 0) + 1);
|
||||
}
|
||||
const hessenIds: string[] = [];
|
||||
for (const [id, entry] of catalog) {
|
||||
if (entry.gebiete.includes("Hessen")) hessenIds.push(id);
|
||||
}
|
||||
|
||||
function hessenArrived(): number {
|
||||
let arrived = 0;
|
||||
for (const id of hessenIds) if (cached.has(id)) arrived += 1;
|
||||
return arrived;
|
||||
}
|
||||
|
||||
let viewport = { ...options.viewport };
|
||||
@@ -160,26 +167,16 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
let seenStandort = standort !== null;
|
||||
let personMovedView = false;
|
||||
let personEnteredZiel = false;
|
||||
let ziel: Point | null = null;
|
||||
let ziele = restoreZiele(markState.ziele);
|
||||
let koordinateNotice: string | null = null;
|
||||
let koordinate = { gitter: "32U MA", rechtswert: "", hochwert: "" };
|
||||
let queue: string[] = [];
|
||||
let hessenFill: string[] | null = null;
|
||||
let fetchesHeld = false;
|
||||
const inFlight = new Map<string, AbortController>();
|
||||
const failed = new Set<string>();
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function arrivedCounts(): Map<string, number> {
|
||||
const counts = new Map(uebersicht.gebiete.map((gebiet) => [gebiet.name, 0]));
|
||||
for (const id of cached) {
|
||||
const entry = catalog.get(id);
|
||||
if (!entry) continue;
|
||||
for (const name of entry.gebiete) {
|
||||
counts.set(name, (counts.get(name) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function clamp(resolution: number): number {
|
||||
const maxResolution = maxResolutionMeters(viewport, config);
|
||||
return Math.min(Math.max(resolution, config.minResolutionMeters), maxResolution);
|
||||
@@ -211,8 +208,23 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
for (const listener of listeners) listener();
|
||||
}
|
||||
|
||||
function persistMarks(): void {
|
||||
void marks.write({ heruntergeladen: [...heruntergeladen] });
|
||||
function persistZiele(): void {
|
||||
void marks.write({ heruntergeladen: [], ziele: ziele.map((item) => item.koordinate) });
|
||||
}
|
||||
|
||||
function restoreZiele(canonical: string[]): { koordinate: string; point: Point }[] {
|
||||
const restored: { koordinate: string; point: Point }[] = [];
|
||||
for (const koordinate of canonical) {
|
||||
const parts = /^(\d{1,2}[C-HJ-NP-X] [A-HJ-NP-Z]{2}) (\d{5}) (\d{5})$/.exec(koordinate);
|
||||
if (!parts) continue;
|
||||
const parsed = parseKoordinate(
|
||||
{ gitter: parts[1], rechtswert: parts[2], hochwert: parts[3] },
|
||||
(point) => uebersicht.blaetter.some((blatt) => coversPoint(blatt, point)),
|
||||
);
|
||||
if (!parsed.ok || parsed.koordinate !== koordinate) continue;
|
||||
restored.push({ koordinate, point: parsed.point });
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
|
||||
function kachelViews(): KachelView[] {
|
||||
@@ -228,7 +240,11 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
views.push({ id: cell.id, appearance: "drawn" });
|
||||
continue;
|
||||
}
|
||||
const willFetch = appOpen && online && !failed.has(cell.id);
|
||||
const willFetch =
|
||||
appOpen &&
|
||||
online &&
|
||||
!failed.has(cell.id) &&
|
||||
(queue.includes(cell.id) || inFlight.has(cell.id));
|
||||
views.push({
|
||||
id: cell.id,
|
||||
appearance: willFetch ? "blank-downloading" : "blank-not-on-device",
|
||||
@@ -238,7 +254,15 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
return views;
|
||||
}
|
||||
|
||||
function releaseFetches(): void {
|
||||
fetchesHeld = false;
|
||||
}
|
||||
|
||||
function rebuild(): void {
|
||||
if (fetchesHeld) {
|
||||
queue = [];
|
||||
return;
|
||||
}
|
||||
const rect = viewRect(center, resolution, viewport);
|
||||
const scale = scaleIndex(uebersicht.resolutions, resolution);
|
||||
const viewNow = cellsInView(scale, rect, uebersicht.resolutions, uebersicht.tileSizePx)
|
||||
@@ -247,17 +271,6 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
for (const id of failed) {
|
||||
if (!viewNow.includes(id)) failed.delete(id);
|
||||
}
|
||||
const viewAny = new Set<string>();
|
||||
for (let zi = 0; zi < uebersicht.resolutions.length; zi += 1) {
|
||||
for (const cell of cellsInView(zi, rect, uebersicht.resolutions, uebersicht.tileSizePx)) {
|
||||
if (catalog.has(cell.id)) viewAny.add(cell.id);
|
||||
}
|
||||
}
|
||||
const gebietIds: string[] = [];
|
||||
for (const [id, entry] of catalog) {
|
||||
if (cached.has(id)) continue;
|
||||
if (entry.gebiete.some((name) => heruntergeladen.has(name))) gebietIds.push(id);
|
||||
}
|
||||
const ordered: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const push = (id: string) => {
|
||||
@@ -266,10 +279,9 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
ordered.push(id);
|
||||
};
|
||||
for (const id of viewNow) push(id);
|
||||
for (const id of gebietIds) {
|
||||
if (viewAny.has(id)) push(id);
|
||||
if (hessenFill) {
|
||||
for (const id of hessenFill) push(id);
|
||||
}
|
||||
for (const id of gebietIds) push(id);
|
||||
queue = ordered;
|
||||
pump();
|
||||
}
|
||||
@@ -292,6 +304,7 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
cached.add(id);
|
||||
stored.add(id);
|
||||
await cache.put(id, bytes);
|
||||
if (hessenFill?.every((tile) => cached.has(tile))) hessenFill = null;
|
||||
} catch (error) {
|
||||
if (!isAbort(error)) failed.add(id);
|
||||
} finally {
|
||||
@@ -310,21 +323,6 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
}
|
||||
|
||||
function snapshot(): KarteSnapshot {
|
||||
const arrived = arrivedCounts();
|
||||
const gebiete: GebietStatus[] = uebersicht.gebiete.map((gebiet) => ({
|
||||
name: gebiet.name,
|
||||
heruntergeladen: heruntergeladen.has(gebiet.name),
|
||||
arrived: arrived.get(gebiet.name) ?? 0,
|
||||
total: totals.get(gebiet.name) ?? 0,
|
||||
}));
|
||||
const distance =
|
||||
ziel && standort
|
||||
? {
|
||||
metres: Math.hypot(ziel.easting - standort.easting, ziel.northing - standort.northing),
|
||||
text: "",
|
||||
}
|
||||
: null;
|
||||
if (distance) distance.text = formatDistance(distance.metres);
|
||||
const views = kachelViews();
|
||||
const missingWhileOffline =
|
||||
!online && views.some((kachel) => kachel.appearance === "blank-not-on-device");
|
||||
@@ -333,18 +331,25 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
center: { ...center },
|
||||
resolution,
|
||||
northUp: true,
|
||||
ziel: ziel ? { ...ziel } : null,
|
||||
ziele: ziele.map((item) => ({
|
||||
koordinate: item.koordinate,
|
||||
point: { ...item.point },
|
||||
entfernung: entfernungTo(standort, item.point),
|
||||
})),
|
||||
standort: standort ? { ...standort } : null,
|
||||
distance,
|
||||
notice,
|
||||
koordinate: { ...koordinate },
|
||||
gebiete,
|
||||
hessen: { arrived: hessenArrived(), total: hessenIds.length },
|
||||
visit: visitOf(),
|
||||
kacheln: views,
|
||||
};
|
||||
}
|
||||
|
||||
function knownGebiet(name: string): boolean {
|
||||
return uebersicht.gebiete.some((gebiet) => gebiet.name === name);
|
||||
function visitOf(): { arrived: number; total: number } | null {
|
||||
if (!hessenFill) return null;
|
||||
let arrived = 0;
|
||||
for (const id of hessenFill) if (cached.has(id)) arrived += 1;
|
||||
return { arrived, total: hessenFill.length };
|
||||
}
|
||||
|
||||
const session: KarteSession = {
|
||||
@@ -354,18 +359,21 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
setViewport(next) {
|
||||
releaseFetches();
|
||||
viewport = { ...next };
|
||||
resolution = clamp(resolution);
|
||||
rebuild();
|
||||
publish();
|
||||
},
|
||||
panTo(next) {
|
||||
releaseFetches();
|
||||
personMovedView = true;
|
||||
center = { ...next };
|
||||
rebuild();
|
||||
publish();
|
||||
},
|
||||
zoomTo(next) {
|
||||
releaseFetches();
|
||||
resolution = clamp(next);
|
||||
rebuild();
|
||||
publish();
|
||||
@@ -379,6 +387,7 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
}
|
||||
standort = { ...next };
|
||||
if (!seenStandort && !personMovedView && !personEnteredZiel) {
|
||||
releaseFetches();
|
||||
center = { ...next };
|
||||
resolution = clamp(config.openResolutionMeters);
|
||||
rebuild();
|
||||
@@ -388,8 +397,11 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
},
|
||||
setAppOpen(open) {
|
||||
appOpen = open;
|
||||
if (!open) abortInFlight(() => true);
|
||||
else {
|
||||
if (!open) {
|
||||
hessenFill = null;
|
||||
abortInFlight(() => true);
|
||||
} else {
|
||||
releaseFetches();
|
||||
failed.clear();
|
||||
rebuild();
|
||||
}
|
||||
@@ -399,6 +411,7 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
online = next;
|
||||
if (!next) abortInFlight(() => true);
|
||||
else {
|
||||
releaseFetches();
|
||||
failed.clear();
|
||||
rebuild();
|
||||
}
|
||||
@@ -413,78 +426,71 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
||||
publish();
|
||||
return;
|
||||
}
|
||||
if (ziele.some((item) => item.koordinate === parsed.koordinate)) {
|
||||
koordinateNotice = "Diese Koordinate ist schon ein Ziel.";
|
||||
publish();
|
||||
return;
|
||||
}
|
||||
koordinateNotice = null;
|
||||
koordinate = {
|
||||
gitter: input.gitter,
|
||||
rechtswert: input.rechtswert,
|
||||
hochwert: input.hochwert,
|
||||
};
|
||||
ziel = parsed.point;
|
||||
ziele = [{ koordinate: parsed.koordinate, point: parsed.point }, ...ziele];
|
||||
personEnteredZiel = true;
|
||||
releaseFetches();
|
||||
center = { ...parsed.point };
|
||||
resolution = clamp(config.openResolutionMeters);
|
||||
persistZiele();
|
||||
rebuild();
|
||||
publish();
|
||||
},
|
||||
deleteZiel(koordinate) {
|
||||
const next = ziele.filter((item) => item.koordinate !== koordinate);
|
||||
if (next.length === ziele.length) return;
|
||||
ziele = next;
|
||||
persistZiele();
|
||||
publish();
|
||||
},
|
||||
centerOnStandort() {
|
||||
if (!standort) return;
|
||||
releaseFetches();
|
||||
personMovedView = true;
|
||||
center = { ...standort };
|
||||
resolution = clamp(config.openResolutionMeters);
|
||||
rebuild();
|
||||
publish();
|
||||
},
|
||||
centerOnZiel() {
|
||||
if (!ziel) return;
|
||||
centerOnZiel(koordinate) {
|
||||
const found = ziele.find((item) => item.koordinate === koordinate);
|
||||
if (!found) return;
|
||||
releaseFetches();
|
||||
personMovedView = true;
|
||||
center = { ...ziel };
|
||||
center = { ...found.point };
|
||||
resolution = clamp(config.openResolutionMeters);
|
||||
rebuild();
|
||||
publish();
|
||||
},
|
||||
startGebiet(name) {
|
||||
if (!knownGebiet(name)) return;
|
||||
heruntergeladen.add(name);
|
||||
persistMarks();
|
||||
rebuild();
|
||||
publish();
|
||||
},
|
||||
removeGebiet(name) {
|
||||
if (!knownGebiet(name)) return;
|
||||
heruntergeladen.delete(name);
|
||||
const removeIds: string[] = [];
|
||||
for (const id of cached) {
|
||||
const entry = catalog.get(id);
|
||||
if (!entry?.gebiete.includes(name)) continue;
|
||||
const kept = entry.gebiete.some(
|
||||
(gebiet) => gebiet !== name && heruntergeladen.has(gebiet),
|
||||
);
|
||||
if (!kept) removeIds.push(id);
|
||||
}
|
||||
for (const id of removeIds) {
|
||||
cached.delete(id);
|
||||
stored.delete(id);
|
||||
}
|
||||
abortInFlight((id) => {
|
||||
const entry = catalog.get(id);
|
||||
if (!entry?.gebiete.includes(name)) return false;
|
||||
return !entry.gebiete.some((gebiet) => gebiet !== name && heruntergeladen.has(gebiet));
|
||||
});
|
||||
if (removeIds.length > 0) void cache.delete(removeIds);
|
||||
persistMarks();
|
||||
startHessen() {
|
||||
if (hessenFill) return;
|
||||
const missing = hessenIds.filter((id) => !cached.has(id));
|
||||
if (missing.length === 0) return;
|
||||
releaseFetches();
|
||||
hessenFill = missing;
|
||||
rebuild();
|
||||
publish();
|
||||
},
|
||||
deleteCache() {
|
||||
fetchesHeld = true;
|
||||
hessenFill = null;
|
||||
abortInFlight(() => true);
|
||||
const ids = [...stored];
|
||||
cached.clear();
|
||||
stored.clear();
|
||||
heruntergeladen.clear();
|
||||
failed.clear();
|
||||
queue = [];
|
||||
void cache.delete(ids);
|
||||
persistMarks();
|
||||
rebuild();
|
||||
publish();
|
||||
},
|
||||
};
|
||||
|
||||
+19
-16
@@ -33,30 +33,33 @@ export type KachelAppearance =
|
||||
|
||||
export type KachelView = { id: string; appearance: KachelAppearance };
|
||||
|
||||
export type GebietStatus = {
|
||||
name: string;
|
||||
heruntergeladen: boolean;
|
||||
arrived: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type KoordinateText = {
|
||||
gitter: string;
|
||||
rechtswert: string;
|
||||
hochwert: string;
|
||||
};
|
||||
|
||||
/** What the screen shows. Distance text is German (`500 m`, `1,5 km`). */
|
||||
/** Straight-line Entfernung. Text is German (`500 m`, `1,5 km`). */
|
||||
export type Entfernung = { metres: number; text: string };
|
||||
|
||||
/** One named place. `koordinate` is the canonical writing, newest list first. */
|
||||
export type Ziel = {
|
||||
koordinate: string;
|
||||
point: Point;
|
||||
entfernung: Entfernung | null;
|
||||
};
|
||||
|
||||
/** What the screen shows. */
|
||||
export type KarteSnapshot = {
|
||||
center: Point;
|
||||
resolution: number;
|
||||
northUp: true;
|
||||
ziel: Point | null;
|
||||
ziele: Ziel[];
|
||||
standort: Point | null;
|
||||
distance: { metres: number; text: string } | null;
|
||||
notice: string | null;
|
||||
koordinate: KoordinateText;
|
||||
gebiete: GebietStatus[];
|
||||
hessen: { arrived: number; total: number };
|
||||
visit: { arrived: number; total: number } | null;
|
||||
kacheln: KachelView[];
|
||||
};
|
||||
|
||||
@@ -67,8 +70,8 @@ export interface CachePort {
|
||||
}
|
||||
|
||||
export interface MarksPort {
|
||||
read(): Promise<{ heruntergeladen: string[] }>;
|
||||
write(state: { heruntergeladen: string[] }): Promise<void>;
|
||||
read(): Promise<{ heruntergeladen: string[]; ziele: string[] }>;
|
||||
write(state: { heruntergeladen: string[]; ziele: string[] }): Promise<void>;
|
||||
}
|
||||
|
||||
/** Fetches Kachel bytes. The session decides when, and stores them in the Cache. */
|
||||
@@ -99,9 +102,9 @@ export type KarteSession = {
|
||||
setAppOpen(open: boolean): void;
|
||||
setOnline(online: boolean): void;
|
||||
enterKoordinate(input: KoordinateText): void;
|
||||
deleteZiel(koordinate: string): void;
|
||||
centerOnStandort(): void;
|
||||
centerOnZiel(): void;
|
||||
startGebiet(name: string): void;
|
||||
removeGebiet(name: string): void;
|
||||
centerOnZiel(koordinate: string): void;
|
||||
startHessen(): void;
|
||||
deleteCache(): void;
|
||||
};
|
||||
|
||||
+32
-6
@@ -78,11 +78,11 @@ input {
|
||||
}
|
||||
|
||||
.fold-toggle,
|
||||
.cache-toggle {
|
||||
.einstellungen-toggle {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.cache-toggle[aria-expanded="true"] {
|
||||
.einstellungen-toggle[aria-expanded="true"] {
|
||||
background: #1e3328;
|
||||
color: #e7e1d4;
|
||||
}
|
||||
@@ -153,6 +153,18 @@ button.secondary {
|
||||
color: #1e3328;
|
||||
}
|
||||
|
||||
button.hessen-download {
|
||||
background: #b42318;
|
||||
border-color: #b42318;
|
||||
color: #e7e1d4;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #e7e1d4;
|
||||
color: #6f675c;
|
||||
border-color: #c4bbaa;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -162,16 +174,30 @@ button.secondary {
|
||||
}
|
||||
|
||||
.gebiet p,
|
||||
.distance,
|
||||
.notice,
|
||||
.boot-error {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.distance {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
.ziel-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ziel-list li {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.ziel-list .ziel-open {
|
||||
flex: 1 1 auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.notice {
|
||||
|
||||
+21
-3
@@ -163,9 +163,8 @@ export function createFieldMap(
|
||||
});
|
||||
|
||||
const markers = new VectorSource();
|
||||
const ziel = new Feature({ geometry: new OlPoint([0, 0]) });
|
||||
const zielStyle = markStyle(ZIEL);
|
||||
const standort = new Feature({ geometry: new OlPoint([0, 0]) });
|
||||
ziel.setStyle(markStyle(ZIEL));
|
||||
standort.setStyle(markStyle(STANDORT));
|
||||
|
||||
const opening = session.snapshot();
|
||||
@@ -268,7 +267,26 @@ export function createFieldMap(
|
||||
|
||||
return {
|
||||
apply(snapshot) {
|
||||
place(markers, ziel, snapshot.ziel);
|
||||
const wanted = new Set(snapshot.ziele.map((ziel) => ziel.koordinate));
|
||||
for (const feature of [...markers.getFeatures()]) {
|
||||
if (feature === standort) continue;
|
||||
const id = feature.getId();
|
||||
if (typeof id !== "string" || !wanted.has(id)) markers.removeFeature(feature);
|
||||
}
|
||||
for (const ziel of snapshot.ziele) {
|
||||
const existing = markers.getFeatureById(ziel.koordinate);
|
||||
const geometry = existing?.getGeometry();
|
||||
if (geometry instanceof OlPoint) {
|
||||
geometry.setCoordinates([ziel.point.easting, ziel.point.northing]);
|
||||
continue;
|
||||
}
|
||||
const feature = new Feature({
|
||||
geometry: new OlPoint([ziel.point.easting, ziel.point.northing]),
|
||||
});
|
||||
feature.setId(ziel.koordinate);
|
||||
feature.setStyle(zielStyle);
|
||||
markers.addFeature(feature);
|
||||
}
|
||||
place(markers, standort, snapshot.standort);
|
||||
|
||||
const current = map.getView();
|
||||
|
||||
+57
-77
@@ -1,9 +1,6 @@
|
||||
import type { GebietStatus, KarteSession, KarteSnapshot, Uebersicht } from "../karte/types";
|
||||
import type { KarteSession, KarteSnapshot, Uebersicht } from "../karte/types";
|
||||
import { createFieldMap } from "./map";
|
||||
|
||||
type ConfirmKind = "hessen" | "cache";
|
||||
type GebietMode = "download" | "remove" | "none";
|
||||
|
||||
export function mountScreen(
|
||||
root: HTMLElement,
|
||||
session: KarteSession,
|
||||
@@ -20,11 +17,11 @@ export function mountScreen(
|
||||
<div class="panel-head">
|
||||
<h1>Karte</h1>
|
||||
<div class="head-actions">
|
||||
<button type="button" class="secondary fold-toggle" aria-expanded="true" aria-controls="koordinaten">Einklappen</button>
|
||||
<button type="button" class="secondary cache-toggle" aria-expanded="false" aria-controls="cache">Cache</button>
|
||||
<button type="button" class="secondary fold-toggle" aria-expanded="true" aria-controls="ziele">Einklappen</button>
|
||||
<button type="button" class="secondary einstellungen-toggle" aria-expanded="false" aria-controls="einstellungen">Einstellungen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="koordinaten" class="fold">
|
||||
<div id="ziele" class="fold">
|
||||
<form>
|
||||
<div class="fields">
|
||||
<label>Gitter <input name="gitter" type="text" autocomplete="off" spellcheck="false" autocapitalize="off" /></label>
|
||||
@@ -33,16 +30,15 @@ export function mountScreen(
|
||||
</div>
|
||||
<button type="submit">Ziel setzen</button>
|
||||
</form>
|
||||
<ul class="ziel-list"></ul>
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary">Auf Standort</button>
|
||||
<button type="button" class="secondary">Auf Ziel</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="distance" hidden></p>
|
||||
<div id="cache" class="fold" hidden>
|
||||
<div id="einstellungen" class="fold" hidden>
|
||||
<div class="gebiet">
|
||||
<p hidden></p>
|
||||
<button type="button"></button>
|
||||
<button type="button" class="hessen-download">Hessen herunterladen</button>
|
||||
</div>
|
||||
<button type="button" class="secondary wipe">Ganzen Cache löschen</button>
|
||||
</div>
|
||||
@@ -61,17 +57,16 @@ export function mountScreen(
|
||||
|
||||
root.append(panel, mapElement, dialog);
|
||||
|
||||
const fold = panel.querySelector<HTMLElement>("#koordinaten");
|
||||
const fold = panel.querySelector<HTMLElement>("#ziele");
|
||||
const foldToggle = panel.querySelector<HTMLButtonElement>(".fold-toggle");
|
||||
const cacheFold = panel.querySelector<HTMLElement>("#cache");
|
||||
const cacheToggle = panel.querySelector<HTMLButtonElement>(".cache-toggle");
|
||||
const einstellungenFold = panel.querySelector<HTMLElement>("#einstellungen");
|
||||
const einstellungenToggle = panel.querySelector<HTMLButtonElement>(".einstellungen-toggle");
|
||||
const form = panel.querySelector("form");
|
||||
const gitter = panel.querySelector<HTMLInputElement>('input[name="gitter"]');
|
||||
const rechtswert = panel.querySelector<HTMLInputElement>('input[name="rechtswert"]');
|
||||
const hochwert = panel.querySelector<HTMLInputElement>('input[name="hochwert"]');
|
||||
const distance = panel.querySelector<HTMLElement>(".distance");
|
||||
const standortButton = panel.querySelector<HTMLButtonElement>(".actions button:nth-child(1)");
|
||||
const zielButton = panel.querySelector<HTMLButtonElement>(".actions button:nth-child(2)");
|
||||
const zielList = panel.querySelector<HTMLElement>(".ziel-list");
|
||||
const standortButton = panel.querySelector<HTMLButtonElement>(".actions button");
|
||||
const gebietBlock = panel.querySelector<HTMLElement>(".gebiet");
|
||||
const gebietText = panel.querySelector<HTMLElement>(".gebiet p");
|
||||
const gebietButton = panel.querySelector<HTMLButtonElement>(".gebiet button");
|
||||
@@ -85,15 +80,14 @@ export function mountScreen(
|
||||
if (
|
||||
!fold ||
|
||||
!foldToggle ||
|
||||
!cacheFold ||
|
||||
!cacheToggle ||
|
||||
!einstellungenFold ||
|
||||
!einstellungenToggle ||
|
||||
!form ||
|
||||
!gitter ||
|
||||
!rechtswert ||
|
||||
!hochwert ||
|
||||
!distance ||
|
||||
!zielList ||
|
||||
!standortButton ||
|
||||
!zielButton ||
|
||||
!gebietBlock ||
|
||||
!gebietText ||
|
||||
!gebietButton ||
|
||||
@@ -113,8 +107,7 @@ export function mountScreen(
|
||||
hochwert.value = opening.koordinate.hochwert;
|
||||
|
||||
const fieldMap = createFieldMap(mapElement, uebersicht, session);
|
||||
let gebietMode: GebietMode = "none";
|
||||
let pending: ConfirmKind | null = null;
|
||||
let pendingWipe = false;
|
||||
let kachelKey = kachelnOf(session.snapshot());
|
||||
|
||||
form.addEventListener("submit", (event) => {
|
||||
@@ -130,38 +123,36 @@ export function mountScreen(
|
||||
const open = fold.hasAttribute("hidden");
|
||||
fold.toggleAttribute("hidden", !open);
|
||||
foldToggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
foldToggle.textContent = open ? "Einklappen" : "Koordinaten";
|
||||
foldToggle.textContent = open ? "Einklappen" : "Ziele";
|
||||
});
|
||||
|
||||
cacheToggle.addEventListener("click", () => {
|
||||
const open = cacheFold.hasAttribute("hidden");
|
||||
cacheFold.toggleAttribute("hidden", !open);
|
||||
cacheToggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
einstellungenToggle.addEventListener("click", () => {
|
||||
const open = einstellungenFold.hasAttribute("hidden");
|
||||
einstellungenFold.toggleAttribute("hidden", !open);
|
||||
einstellungenToggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
});
|
||||
|
||||
standortButton.addEventListener("click", () => session.centerOnStandort());
|
||||
zielButton.addEventListener("click", () => session.centerOnZiel());
|
||||
gebietButton.addEventListener("click", () => {
|
||||
if (gebietMode === "download") session.startGebiet("Hessen");
|
||||
if (gebietMode === "remove") ask("hessen");
|
||||
if (gebietButton.disabled) return;
|
||||
session.startHessen();
|
||||
});
|
||||
wipeButton.addEventListener("click", () => ask("cache"));
|
||||
wipeButton.addEventListener("click", () => askToWipe());
|
||||
|
||||
confirmButton.addEventListener("click", () => {
|
||||
const kind = pending;
|
||||
pending = null;
|
||||
const wipe = pendingWipe;
|
||||
pendingWipe = false;
|
||||
dialog.close();
|
||||
if (kind === "hessen") session.removeGebiet("Hessen");
|
||||
if (kind === "cache") session.deleteCache();
|
||||
if (wipe) session.deleteCache();
|
||||
});
|
||||
|
||||
const dismiss = () => {
|
||||
pending = null;
|
||||
pendingWipe = false;
|
||||
if (dialog.open) dialog.close();
|
||||
};
|
||||
cancelButton.addEventListener("click", dismiss);
|
||||
dialog.addEventListener("cancel", () => {
|
||||
pending = null;
|
||||
pendingWipe = false;
|
||||
});
|
||||
|
||||
const render = () => {
|
||||
@@ -176,12 +167,21 @@ export function mountScreen(
|
||||
};
|
||||
|
||||
const renderPanel = (snapshot: KarteSnapshot) => {
|
||||
if (snapshot.distance) {
|
||||
distance.hidden = false;
|
||||
distance.textContent = `Entfernung ${snapshot.distance.text}`;
|
||||
} else {
|
||||
distance.hidden = true;
|
||||
distance.textContent = "";
|
||||
zielList.replaceChildren();
|
||||
for (const ziel of snapshot.ziele) {
|
||||
const row = document.createElement("li");
|
||||
const open = document.createElement("button");
|
||||
open.type = "button";
|
||||
open.className = "secondary ziel-open";
|
||||
open.textContent = ziel.entfernung ? `${ziel.koordinate} ${ziel.entfernung.text}` : ziel.koordinate;
|
||||
open.addEventListener("click", () => session.centerOnZiel(ziel.koordinate));
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.className = "secondary";
|
||||
remove.textContent = "Löschen";
|
||||
remove.addEventListener("click", () => session.deleteZiel(ziel.koordinate));
|
||||
row.append(open, remove);
|
||||
zielList.append(row);
|
||||
}
|
||||
|
||||
if (snapshot.notice) {
|
||||
@@ -192,46 +192,26 @@ export function mountScreen(
|
||||
notice.textContent = "";
|
||||
}
|
||||
|
||||
renderHessen(snapshot.gebiete.find((gebiet) => gebiet.name === "Hessen"));
|
||||
};
|
||||
|
||||
const renderHessen = (hessen: GebietStatus | undefined) => {
|
||||
if (!hessen) {
|
||||
gebietBlock.hidden = true;
|
||||
gebietMode = "none";
|
||||
return;
|
||||
}
|
||||
const missing = snapshot.hessen.arrived < snapshot.hessen.total;
|
||||
gebietBlock.hidden = false;
|
||||
if (!hessen.heruntergeladen) {
|
||||
gebietButton.textContent = "Hessen herunterladen";
|
||||
gebietButton.disabled = !missing;
|
||||
gebietButton.classList.toggle("hessen-download", missing);
|
||||
if (snapshot.visit) {
|
||||
gebietText.hidden = false;
|
||||
gebietText.textContent = `Kacheln: ${snapshot.visit.arrived} von ${snapshot.visit.total}`;
|
||||
} else {
|
||||
gebietText.hidden = true;
|
||||
gebietText.textContent = "";
|
||||
gebietButton.textContent = "Hessen herunterladen";
|
||||
gebietButton.classList.remove("secondary");
|
||||
gebietMode = "download";
|
||||
return;
|
||||
}
|
||||
gebietMode = "remove";
|
||||
gebietButton.textContent = "Hessen entfernen";
|
||||
gebietButton.classList.add("secondary");
|
||||
gebietText.hidden = false;
|
||||
gebietText.textContent =
|
||||
hessen.arrived === hessen.total
|
||||
? "Hessen heruntergeladen"
|
||||
: `Hessen: ${hessen.arrived} von ${hessen.total} Kacheln`;
|
||||
};
|
||||
|
||||
const ask = (kind: ConfirmKind) => {
|
||||
const askToWipe = () => {
|
||||
if (dialog.open) return;
|
||||
pending = kind;
|
||||
if (kind === "hessen") {
|
||||
question.textContent = "Hessen entfernen?";
|
||||
explain.textContent = "Die Kacheln von Hessen werden gelöscht.";
|
||||
confirmButton.textContent = "Entfernen";
|
||||
} else {
|
||||
question.textContent = "Den ganzen Cache löschen?";
|
||||
explain.textContent = "Alle Kacheln werden gelöscht.";
|
||||
confirmButton.textContent = "Löschen";
|
||||
}
|
||||
pendingWipe = true;
|
||||
question.textContent = "Den ganzen Cache löschen?";
|
||||
explain.textContent = "Alle Kacheln werden gelöscht.";
|
||||
confirmButton.textContent = "Löschen";
|
||||
dialog.showModal();
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user