Let a tap name a Ziel and download the view in front of you.
The same queue fetches that view at every prepared scale when Download-Knopf is on. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import type { MarksPort } from "../karte/types";
|
import type { MarksPort, RememberedMarks } from "../karte/types";
|
||||||
|
|
||||||
const DB_NAME = "karte";
|
const DB_NAME = "karte";
|
||||||
const STORE = "marks";
|
const STORE = "marks";
|
||||||
@@ -21,16 +21,23 @@ function stringsFrom(value: unknown): string[] {
|
|||||||
return value.filter((item): item is string => typeof item === "string");
|
return value.filter((item): item is string => typeof item === "string");
|
||||||
}
|
}
|
||||||
|
|
||||||
function rememberedFrom(value: unknown): { heruntergeladen: string[]; ziele: string[] } {
|
function rememberedFrom(value: unknown): RememberedMarks {
|
||||||
if (Array.isArray(value)) return { heruntergeladen: stringsFrom(value), ziele: [] };
|
if (Array.isArray(value)) {
|
||||||
|
return { heruntergeladen: stringsFrom(value), ziele: [], downloadKnopf: false };
|
||||||
|
}
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
const record = value as { heruntergeladen?: unknown; ziele?: unknown };
|
const record = value as {
|
||||||
|
heruntergeladen?: unknown;
|
||||||
|
ziele?: unknown;
|
||||||
|
downloadKnopf?: unknown;
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
heruntergeladen: stringsFrom(record.heruntergeladen),
|
heruntergeladen: stringsFrom(record.heruntergeladen),
|
||||||
ziele: stringsFrom(record.ziele),
|
ziele: stringsFrom(record.ziele),
|
||||||
|
downloadKnopf: record.downloadKnopf === true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { heruntergeladen: [], ziele: [] };
|
return { heruntergeladen: [], ziele: [], downloadKnopf: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
function settle(tx: IDBTransaction, value: () => unknown): Promise<unknown> {
|
function settle(tx: IDBTransaction, value: () => unknown): Promise<unknown> {
|
||||||
@@ -60,7 +67,11 @@ export function createMarksDb(): MarksPort {
|
|||||||
try {
|
try {
|
||||||
const tx = db.transaction(STORE, "readwrite");
|
const tx = db.transaction(STORE, "readwrite");
|
||||||
tx.objectStore(STORE).put(
|
tx.objectStore(STORE).put(
|
||||||
{ heruntergeladen: state.heruntergeladen, ziele: state.ziele },
|
{
|
||||||
|
heruntergeladen: state.heruntergeladen,
|
||||||
|
ziele: state.ziele,
|
||||||
|
downloadKnopf: state.downloadKnopf,
|
||||||
|
},
|
||||||
KEY,
|
KEY,
|
||||||
);
|
);
|
||||||
await settle(tx, () => undefined);
|
await settle(tx, () => undefined);
|
||||||
|
|||||||
+3
-1
@@ -18,13 +18,15 @@ export function createMemoryCache(initial: string[] = []): CachePort {
|
|||||||
export function createMemoryMarks(initial: string[] = []): MarksPort {
|
export function createMemoryMarks(initial: string[] = []): MarksPort {
|
||||||
let heruntergeladen = [...initial];
|
let heruntergeladen = [...initial];
|
||||||
let ziele: string[] = [];
|
let ziele: string[] = [];
|
||||||
|
let downloadKnopf = false;
|
||||||
return {
|
return {
|
||||||
async read() {
|
async read() {
|
||||||
return { heruntergeladen: [...heruntergeladen], ziele: [...ziele] };
|
return { heruntergeladen: [...heruntergeladen], ziele: [...ziele], downloadKnopf };
|
||||||
},
|
},
|
||||||
async write(state) {
|
async write(state) {
|
||||||
heruntergeladen = [...state.heruntergeladen];
|
heruntergeladen = [...state.heruntergeladen];
|
||||||
ziele = [...state.ziele];
|
ziele = [...state.ziele];
|
||||||
|
downloadKnopf = state.downloadKnopf;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-2
@@ -1,6 +1,6 @@
|
|||||||
import { toPoint } from "mgrs";
|
import { forward, toPoint } from "mgrs";
|
||||||
import proj4 from "proj4";
|
import proj4 from "proj4";
|
||||||
import type { Point } from "./types";
|
import type { KoordinateText, Point } from "./types";
|
||||||
|
|
||||||
proj4.defs(
|
proj4.defs(
|
||||||
"EPSG:25832",
|
"EPSG:25832",
|
||||||
@@ -10,10 +10,31 @@ proj4.defs(
|
|||||||
const DIGITS = "Rechtswert und Hochwert müssen genau fünf Ziffern haben.";
|
const DIGITS = "Rechtswert und Hochwert müssen genau fünf Ziffern haben.";
|
||||||
const GITTER_NOTICE = "Dieses Gitter ist keine gültige Koordinate.";
|
const GITTER_NOTICE = "Dieses Gitter ist keine gültige Koordinate.";
|
||||||
const BLATT = "Kein Blatt deckt diese Koordinate.";
|
const BLATT = "Kein Blatt deckt diese Koordinate.";
|
||||||
|
const MGRS_1M = /^(\d{1,2}[C-HJ-NP-X])([A-HJ-NP-Z]{2})(\d{5})(\d{5})$/;
|
||||||
|
|
||||||
/** Zone, latitude band, and 100 km square. I and O are not MGRS letters. */
|
/** Zone, latitude band, and 100 km square. I and O are not MGRS letters. */
|
||||||
const GITTER_PATTERN = /^(\d{1,2})([C-HJ-NP-X])([A-HJ-NP-Z]{2})$/;
|
const GITTER_PATTERN = /^(\d{1,2})([C-HJ-NP-X])([A-HJ-NP-Z]{2})$/;
|
||||||
|
|
||||||
|
/** Nearest metre inside the 100 km square, written as Gitter, Rechtswert, and Hochwert. */
|
||||||
|
export function writeKoordinate(point: Point): KoordinateText | null {
|
||||||
|
const easting = Math.round(point.easting);
|
||||||
|
const northing = Math.round(point.northing);
|
||||||
|
const geographic = proj4("EPSG:25832", "EPSG:4326", [easting, northing]);
|
||||||
|
let mgrsText: string;
|
||||||
|
try {
|
||||||
|
mgrsText = forward([geographic[0], geographic[1]], 5);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = MGRS_1M.exec(mgrsText);
|
||||||
|
if (!match) return null;
|
||||||
|
return {
|
||||||
|
gitter: `${match[1]} ${match[2]}`,
|
||||||
|
rechtswert: match[3],
|
||||||
|
hochwert: match[4],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function parseKoordinate(
|
export function parseKoordinate(
|
||||||
input: { gitter: string; rechtswert: string; hochwert: string },
|
input: { gitter: string; rechtswert: string; hochwert: string },
|
||||||
blattCovers: (point: Point) => boolean,
|
blattCovers: (point: Point) => boolean,
|
||||||
|
|||||||
+463
-1
@@ -48,6 +48,7 @@ async function openSheet(options?: {
|
|||||||
clock?: ReturnType<typeof createFetchClock>;
|
clock?: ReturnType<typeof createFetchClock>;
|
||||||
uebersicht?: Uebersicht;
|
uebersicht?: Uebersicht;
|
||||||
maxSpanMeters?: number;
|
maxSpanMeters?: number;
|
||||||
|
minResolutionMeters?: number;
|
||||||
}) {
|
}) {
|
||||||
const cache = options?.cache ?? createMemoryCache();
|
const cache = options?.cache ?? createMemoryCache();
|
||||||
const marks = options?.marks ?? createMemoryMarks();
|
const marks = options?.marks ?? createMemoryMarks();
|
||||||
@@ -57,7 +58,11 @@ async function openSheet(options?: {
|
|||||||
cache,
|
cache,
|
||||||
marks,
|
marks,
|
||||||
kacheln: clock,
|
kacheln: clock,
|
||||||
config: { ...shippedConfig, maxSpanMeters: options?.maxSpanMeters ?? shippedConfig.maxSpanMeters },
|
config: {
|
||||||
|
...shippedConfig,
|
||||||
|
maxSpanMeters: options?.maxSpanMeters ?? shippedConfig.maxSpanMeters,
|
||||||
|
minResolutionMeters: options?.minResolutionMeters ?? shippedConfig.minResolutionMeters,
|
||||||
|
},
|
||||||
viewport: options?.viewport ?? phone,
|
viewport: options?.viewport ?? phone,
|
||||||
standort: options?.standort,
|
standort: options?.standort,
|
||||||
online: options?.online ?? false,
|
online: options?.online ?? false,
|
||||||
@@ -431,6 +436,202 @@ describe("naming a Ziel", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("tapping the Karte", () => {
|
||||||
|
test("a tap within 44 px deletes the nearest Ziel and leaves the view", async () => {
|
||||||
|
const { karte } = await openSheet({ uebersicht: withHole, online: true });
|
||||||
|
karte.enterKoordinate(sample);
|
||||||
|
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "67100", hochwert: "78109" });
|
||||||
|
karte.panTo({ easting: 465_000, northing: 5_580_000 });
|
||||||
|
karte.zoomTo(7);
|
||||||
|
const older = karte.snapshot().ziele[1].point;
|
||||||
|
karte.tap({ easting: older.easting, northing: older.northing - 10 });
|
||||||
|
const snapshot = karte.snapshot();
|
||||||
|
expect(snapshot.ziele.map((ziel) => ziel.koordinate)).toEqual(["32U MA 67100 78109"]);
|
||||||
|
expect(snapshot.center).toEqual({ easting: 465_000, northing: 5_580_000 });
|
||||||
|
expect(snapshot.resolution).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a tie within 44 px deletes the newer Ziel", async () => {
|
||||||
|
const { karte } = await openSheet({ uebersicht: withHole, online: true });
|
||||||
|
karte.enterKoordinate(sample);
|
||||||
|
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "67100", hochwert: "78109" });
|
||||||
|
const [newer, older] = karte.snapshot().ziele;
|
||||||
|
karte.panTo({ easting: 465_000, northing: 5_580_000 });
|
||||||
|
karte.zoomTo(1.25);
|
||||||
|
karte.tap({
|
||||||
|
easting: (newer.point.easting + older.point.easting) / 2,
|
||||||
|
northing: (newer.point.northing + older.point.northing) / 2,
|
||||||
|
});
|
||||||
|
const snapshot = karte.snapshot();
|
||||||
|
expect(snapshot.ziele.map((ziel) => ziel.koordinate)).toEqual(["32U MA 67030 78109"]);
|
||||||
|
expect(snapshot.center).toEqual({ easting: 465_000, northing: 5_580_000 });
|
||||||
|
expect(snapshot.resolution).toBe(1.25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a tap farther than 44 px adds that metre and leaves the view and the typed fields", async () => {
|
||||||
|
const { karte } = await openSheet({ uebersicht: withHole, online: true });
|
||||||
|
karte.enterKoordinate({ gitter: "32u ma", rechtswert: "67030", hochwert: "78109" });
|
||||||
|
karte.panTo({ easting: 465_000, northing: 5_580_000 });
|
||||||
|
karte.zoomTo(10);
|
||||||
|
const typed = karte.snapshot().koordinate;
|
||||||
|
karte.tap({ easting: 481_000, northing: 5_578_109 });
|
||||||
|
const snapshot = karte.snapshot();
|
||||||
|
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(Math.abs(snapshot.ziele[0].point.northing - 5_578_109)).toBeLessThan(1);
|
||||||
|
expect(snapshot.center).toEqual({ easting: 465_000, northing: 5_580_000 });
|
||||||
|
expect(snapshot.resolution).toBe(10);
|
||||||
|
expect(snapshot.koordinate).toEqual(typed);
|
||||||
|
expect(snapshot.notice).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a repeated Koordinate from a tap is refused and changes nothing else", async () => {
|
||||||
|
const { karte } = await openSheet({
|
||||||
|
uebersicht: wideSquare,
|
||||||
|
online: true,
|
||||||
|
minResolutionMeters: 0.001,
|
||||||
|
});
|
||||||
|
karte.enterKoordinate({ gitter: "32u ma", rechtswert: "67030", hochwert: "78109" });
|
||||||
|
const ziel = karte.snapshot().ziele[0];
|
||||||
|
karte.panTo({ easting: 420_000, northing: 5_520_000 });
|
||||||
|
karte.zoomTo(0.001);
|
||||||
|
const kept = karte.snapshot();
|
||||||
|
// 0.2 m west stays in this metre, and at 0.001 m/px it is outside 44 px.
|
||||||
|
karte.tap({ easting: ziel.point.easting - 0.2, northing: ziel.point.northing });
|
||||||
|
const snapshot = karte.snapshot();
|
||||||
|
expect(snapshot.notice).toBe("Diese Koordinate ist schon ein Ziel.");
|
||||||
|
expect(snapshot.ziele).toEqual(kept.ziele);
|
||||||
|
expect(snapshot.center).toEqual(kept.center);
|
||||||
|
expect(snapshot.resolution).toBe(0.001);
|
||||||
|
expect(snapshot.koordinate).toEqual(kept.koordinate);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the 44 px radius is inclusive, and one metre farther names that metre", async () => {
|
||||||
|
const { karte } = await openSheet({ uebersicht: wideSquare, online: true });
|
||||||
|
karte.enterKoordinate(sample);
|
||||||
|
const onTheEdge = karte.snapshot().ziele[0].point;
|
||||||
|
karte.panTo({ easting: 420_000, northing: 5_520_000 });
|
||||||
|
karte.zoomTo(10);
|
||||||
|
const typed = karte.snapshot().koordinate;
|
||||||
|
karte.tap({ easting: onTheEdge.easting, northing: onTheEdge.northing + 440 });
|
||||||
|
expect(karte.snapshot().ziele).toEqual([]);
|
||||||
|
expect(karte.snapshot().center).toEqual({ easting: 420_000, northing: 5_520_000 });
|
||||||
|
expect(karte.snapshot().resolution).toBe(10);
|
||||||
|
expect(karte.snapshot().koordinate).toEqual(typed);
|
||||||
|
|
||||||
|
karte.enterKoordinate(sample);
|
||||||
|
const again = karte.snapshot().ziele[0].point;
|
||||||
|
karte.panTo({ easting: 420_000, northing: 5_520_000 });
|
||||||
|
karte.zoomTo(10);
|
||||||
|
karte.tap({ easting: again.easting, northing: again.northing + 441 });
|
||||||
|
const snapshot = karte.snapshot();
|
||||||
|
expect(snapshot.ziele.map((ziel) => ziel.koordinate)).toEqual([
|
||||||
|
"32U MA 67030 78550",
|
||||||
|
"32U MA 67030 78109",
|
||||||
|
]);
|
||||||
|
expect(snapshot.center).toEqual({ easting: 420_000, northing: 5_520_000 });
|
||||||
|
expect(snapshot.resolution).toBe(10);
|
||||||
|
expect(snapshot.koordinate).toEqual(typed);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a tap 100 m away deletes at 7 m per pixel and adds at 1.25 m per pixel", async () => {
|
||||||
|
const { karte } = await openSheet({ uebersicht: wideSquare, online: true });
|
||||||
|
karte.enterKoordinate(sample);
|
||||||
|
const ziel = karte.snapshot().ziele[0].point;
|
||||||
|
karte.panTo({ easting: 420_000, northing: 5_520_000 });
|
||||||
|
karte.zoomTo(1.25);
|
||||||
|
karte.tap({ easting: ziel.easting, northing: ziel.northing + 100 });
|
||||||
|
expect(karte.snapshot().ziele.map((item) => item.koordinate)).toEqual([
|
||||||
|
"32U MA 67030 78209",
|
||||||
|
"32U MA 67030 78109",
|
||||||
|
]);
|
||||||
|
expect(karte.snapshot().center).toEqual({ easting: 420_000, northing: 5_520_000 });
|
||||||
|
expect(karte.snapshot().resolution).toBe(1.25);
|
||||||
|
|
||||||
|
karte.zoomTo(7);
|
||||||
|
karte.tap({ easting: ziel.easting, northing: ziel.northing + 100 });
|
||||||
|
expect(karte.snapshot().ziele.map((item) => item.koordinate)).toEqual(["32U MA 67030 78109"]);
|
||||||
|
karte.tap({ easting: ziel.easting, northing: ziel.northing + 100 });
|
||||||
|
expect(karte.snapshot().ziele).toEqual([]);
|
||||||
|
expect(karte.snapshot().center).toEqual({ easting: 420_000, northing: 5_520_000 });
|
||||||
|
expect(karte.snapshot().resolution).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a tap on the Standort adds a Ziel there when no Ziel is within 44 px", async () => {
|
||||||
|
const standort = { easting: 467_030, northing: 5_578_109 };
|
||||||
|
const { karte } = await openSheet({ uebersicht: wideSquare, online: true, standort });
|
||||||
|
const before = karte.snapshot();
|
||||||
|
karte.tap(standort);
|
||||||
|
const snapshot = karte.snapshot();
|
||||||
|
expect(snapshot.ziele.map((ziel) => ziel.koordinate)).toEqual(["32U MA 67030 78109"]);
|
||||||
|
expect(Math.abs(snapshot.ziele[0].point.easting - standort.easting)).toBeLessThan(1);
|
||||||
|
expect(Math.abs(snapshot.ziele[0].point.northing - standort.northing)).toBeLessThan(1);
|
||||||
|
expect(snapshot.standort).toEqual(standort);
|
||||||
|
expect(snapshot.center).toEqual(before.center);
|
||||||
|
expect(snapshot.resolution).toBe(before.resolution);
|
||||||
|
expect(snapshot.koordinate).toEqual(before.koordinate);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a tap on the Standort deletes a Ziel within 44 px and leaves the Standort", async () => {
|
||||||
|
const { karte } = await openSheet({ uebersicht: wideSquare, online: true });
|
||||||
|
karte.enterKoordinate(sample);
|
||||||
|
const ziel = karte.snapshot().ziele[0].point;
|
||||||
|
const center = karte.snapshot().center;
|
||||||
|
karte.reportStandort(ziel);
|
||||||
|
karte.tap(ziel);
|
||||||
|
expect(karte.snapshot().ziele).toEqual([]);
|
||||||
|
expect(karte.snapshot().standort).toEqual(ziel);
|
||||||
|
expect(karte.snapshot().center).toEqual(center);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a tap no Blatt covers leaves the list, the notice, and the view", async () => {
|
||||||
|
const { karte } = await openSheet({ uebersicht: withHole, online: true });
|
||||||
|
karte.enterKoordinate(sample);
|
||||||
|
karte.enterKoordinate({ gitter: "32U MA", rechtswert: "6703", hochwert: "78109" });
|
||||||
|
karte.panTo({ easting: 465_000, northing: 5_580_000 });
|
||||||
|
karte.zoomTo(10);
|
||||||
|
const kept = karte.snapshot();
|
||||||
|
expect(kept.notice).toBe("Rechtswert und Hochwert müssen genau fünf Ziffern haben.");
|
||||||
|
karte.tap({ easting: 475_000, northing: 5_578_109 });
|
||||||
|
const snapshot = karte.snapshot();
|
||||||
|
expect(snapshot.notice).toBe(kept.notice);
|
||||||
|
expect(snapshot.ziele).toEqual(kept.ziele);
|
||||||
|
expect(snapshot.center).toEqual(kept.center);
|
||||||
|
expect(snapshot.resolution).toBe(kept.resolution);
|
||||||
|
expect(snapshot.koordinate).toEqual(kept.koordinate);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a Ziel added by a tap keeps a later first Standort from moving the view", async () => {
|
||||||
|
const { karte } = await openSheet({ uebersicht: wideSquare, online: true });
|
||||||
|
const opening = karte.snapshot().center;
|
||||||
|
karte.tap({ easting: 481_000, northing: 5_578_109 });
|
||||||
|
karte.reportStandort({ easting: 420_000, northing: 5_520_000 });
|
||||||
|
expect(karte.snapshot().center).toEqual(opening);
|
||||||
|
expect(karte.snapshot().standort).toEqual({ easting: 420_000, northing: 5_520_000 });
|
||||||
|
expect(karte.snapshot().ziele.map((ziel) => ziel.koordinate)).toEqual(["32U MA 81000 78109"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("leaving the page keeps a Ziel added by a tap and drops one removed by a tap", async () => {
|
||||||
|
const cache = createMemoryCache();
|
||||||
|
const marks = createMemoryMarks();
|
||||||
|
const first = await openSheet({ uebersicht: withHole, cache, marks, online: true });
|
||||||
|
first.karte.enterKoordinate(sample);
|
||||||
|
first.karte.tap({ easting: 481_000, northing: 5_578_109 });
|
||||||
|
first.karte.tap(first.karte.snapshot().ziele[1].point);
|
||||||
|
expect(first.karte.snapshot().ziele.map((ziel) => ziel.koordinate)).toEqual([
|
||||||
|
"32U MA 81000 78109",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const kept = await openSheet({ uebersicht: withHole, cache, marks, online: true });
|
||||||
|
expect(kept.karte.snapshot().ziele.map((ziel) => ziel.koordinate)).toEqual([
|
||||||
|
"32U MA 81000 78109",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Standort and Entfernung", () => {
|
describe("Standort and Entfernung", () => {
|
||||||
test("a late first Standort centers the view once", async () => {
|
test("a late first Standort centers the view once", async () => {
|
||||||
const { karte } = await openSheet();
|
const { karte } = await openSheet();
|
||||||
@@ -779,3 +980,264 @@ describe("downloading Hessen", () => {
|
|||||||
expect(appearance(karte, TILE_A)).toBe("blank-not-on-device");
|
expect(appearance(karte, TILE_A)).toBe("blank-not-on-device");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("downloading the current view", () => {
|
||||||
|
test("the Download-Knopf starts off, is remembered, and deleteCache leaves it", async () => {
|
||||||
|
const marks = createMemoryMarks(["Hessen"]);
|
||||||
|
const first = await openSheet({ marks, uebersicht: wideSquare });
|
||||||
|
expect(first.karte.snapshot().downloadKnopf).toBe(false);
|
||||||
|
expect(first.karte.snapshot().ziele).toEqual([]);
|
||||||
|
|
||||||
|
first.karte.setDownloadKnopf(true);
|
||||||
|
expect(first.karte.snapshot().downloadKnopf).toBe(true);
|
||||||
|
first.karte.enterKoordinate(sample);
|
||||||
|
first.karte.deleteCache();
|
||||||
|
expect(first.karte.snapshot().downloadKnopf).toBe(true);
|
||||||
|
expect(first.karte.snapshot().ziele.map((ziel) => ziel.koordinate)).toEqual([
|
||||||
|
"32U MA 67030 78109",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const second = await openSheet({ marks, uebersicht: wideSquare });
|
||||||
|
expect(second.karte.snapshot().downloadKnopf).toBe(true);
|
||||||
|
expect(second.karte.snapshot().ziele.map((ziel) => ziel.koordinate)).toEqual([
|
||||||
|
"32U MA 67030 78109",
|
||||||
|
]);
|
||||||
|
|
||||||
|
second.karte.setDownloadKnopf(false);
|
||||||
|
const third = await openSheet({ marks, uebersicht: wideSquare });
|
||||||
|
expect(third.karte.snapshot().downloadKnopf).toBe(false);
|
||||||
|
expect(third.karte.snapshot().ziele).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("downloadView fetches the finer Kacheln inside a view that covers a Blatt, current scale first", async () => {
|
||||||
|
const opened = await openSheet({
|
||||||
|
viewport: oneTile,
|
||||||
|
uebersicht: {
|
||||||
|
tileSizePx: 10,
|
||||||
|
resolutions: [10, 20],
|
||||||
|
blaetter: [{ minE: 0, minN: 0, maxE: 200, maxN: 200 }],
|
||||||
|
gebiete: [{ name: "Hessen", covers: "all" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
opened.karte.zoomTo(20);
|
||||||
|
opened.karte.panTo({ easting: 100, northing: 100 });
|
||||||
|
opened.karte.downloadView();
|
||||||
|
expect(opened.karte.snapshot().visit).toEqual({ arrived: 0, total: 5 });
|
||||||
|
|
||||||
|
opened.karte.setOnline(true);
|
||||||
|
await opened.clock.tick();
|
||||||
|
opened.karte.setOnline(false);
|
||||||
|
expect(appearance(opened.karte, "1/0/0")).toBe("drawn");
|
||||||
|
opened.karte.zoomTo(10);
|
||||||
|
opened.karte.panTo({ easting: 50, northing: 50 });
|
||||||
|
expect(appearance(opened.karte, "0/0/0")).toBe("blank-not-on-device");
|
||||||
|
opened.karte.panTo({ easting: 150, northing: 150 });
|
||||||
|
expect(appearance(opened.karte, "0/1/1")).toBe("blank-not-on-device");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a later pan does not add Kacheln to the view fill", async () => {
|
||||||
|
const { karte, clock } = await openSheet({ viewport: oneTile });
|
||||||
|
karte.zoomTo(10);
|
||||||
|
karte.panTo({ easting: 50, northing: 50 });
|
||||||
|
karte.downloadView();
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 0, total: 1 });
|
||||||
|
|
||||||
|
karte.panTo({ easting: 250, northing: 50 });
|
||||||
|
karte.zoomTo(30);
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 0, total: 1 });
|
||||||
|
karte.zoomTo(10);
|
||||||
|
|
||||||
|
karte.setOnline(true);
|
||||||
|
await clock.tick();
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 0, total: 1 });
|
||||||
|
await clock.tick();
|
||||||
|
expect(karte.snapshot().visit).toBeNull();
|
||||||
|
karte.setOnline(false);
|
||||||
|
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("blank-not-on-device");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("downloadView skips Kacheln already in the Cache", async () => {
|
||||||
|
const { karte, clock } = await openSheet({
|
||||||
|
viewport: threeTiles,
|
||||||
|
cache: createMemoryCache([TILE_A]),
|
||||||
|
});
|
||||||
|
karte.zoomTo(10);
|
||||||
|
karte.panTo({ easting: 150, northing: 50 });
|
||||||
|
karte.downloadView();
|
||||||
|
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 0, total: 2 });
|
||||||
|
karte.setOnline(true);
|
||||||
|
await clock.tick();
|
||||||
|
await clock.tick();
|
||||||
|
expect(karte.snapshot().visit).toBeNull();
|
||||||
|
expect(appearance(karte, TILE_B)).toBe("drawn");
|
||||||
|
expect(appearance(karte, TILE_C)).toBe("drawn");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("downloadView fetches nothing when the view is already in the Cache", async () => {
|
||||||
|
const { karte, clock } = await openSheet({
|
||||||
|
viewport: oneTile,
|
||||||
|
cache: createMemoryCache([TILE_A]),
|
||||||
|
});
|
||||||
|
karte.zoomTo(10);
|
||||||
|
karte.panTo({ easting: 50, northing: 50 });
|
||||||
|
karte.setOnline(true);
|
||||||
|
karte.downloadView();
|
||||||
|
expect(karte.snapshot().visit).toBeNull();
|
||||||
|
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||||
|
await clock.tick();
|
||||||
|
await clock.tick();
|
||||||
|
karte.setOnline(false);
|
||||||
|
karte.panTo({ easting: 150, northing: 50 });
|
||||||
|
expect(appearance(karte, TILE_B)).toBe("blank-not-on-device");
|
||||||
|
expect(karte.snapshot().visit).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a second downloadView while that fill is unfinished adds nothing", async () => {
|
||||||
|
const { karte, clock } = await openSheet({ viewport: threeTiles });
|
||||||
|
karte.zoomTo(10);
|
||||||
|
karte.panTo({ easting: 150, northing: 50 });
|
||||||
|
karte.downloadView();
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 0, total: 3 });
|
||||||
|
karte.setOnline(true);
|
||||||
|
await clock.tick();
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||||
|
karte.panTo({ easting: 50, northing: 50 });
|
||||||
|
karte.downloadView();
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||||
|
await clock.tick();
|
||||||
|
await clock.tick();
|
||||||
|
expect(karte.snapshot().visit).toBeNull();
|
||||||
|
karte.setOnline(false);
|
||||||
|
karte.zoomTo(10);
|
||||||
|
karte.panTo({ easting: 250, northing: 50 });
|
||||||
|
expect(appearance(karte, TILE_C)).toBe("drawn");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("startHessen while a view fill runs still fetches the missing Hessen Kacheln", async () => {
|
||||||
|
const opened = await openSheet({
|
||||||
|
viewport: oneTile,
|
||||||
|
uebersicht: {
|
||||||
|
tileSizePx: 10,
|
||||||
|
resolutions: [10],
|
||||||
|
blaetter: [{ minE: 0, minN: 0, maxE: 300, maxN: 100 }],
|
||||||
|
gebiete: [{ name: "Hessen", covers: [{ minE: 0, minN: 0, maxE: 200, maxN: 100 }] }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
opened.karte.zoomTo(10);
|
||||||
|
opened.karte.panTo({ easting: 250, northing: 50 });
|
||||||
|
opened.karte.downloadView();
|
||||||
|
expect(opened.karte.snapshot().visit).toEqual({ arrived: 0, total: 1 });
|
||||||
|
expect(opened.karte.snapshot().hessen).toEqual({ arrived: 0, total: 2 });
|
||||||
|
opened.karte.startHessen();
|
||||||
|
expect(opened.karte.snapshot().visit).toEqual({ arrived: 0, total: 3 });
|
||||||
|
opened.karte.setOnline(true);
|
||||||
|
await opened.clock.tick();
|
||||||
|
await opened.clock.tick();
|
||||||
|
await opened.clock.tick();
|
||||||
|
expect(opened.karte.snapshot().visit).toBeNull();
|
||||||
|
expect(opened.karte.snapshot().hessen).toEqual({ arrived: 2, total: 2 });
|
||||||
|
opened.karte.setOnline(false);
|
||||||
|
opened.karte.panTo({ easting: 50, northing: 50 });
|
||||||
|
expect(appearance(opened.karte, TILE_A)).toBe("drawn");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the view fill and the Hessen fill share one visit count, current view first", async () => {
|
||||||
|
const { karte, clock } = await openSheet({ viewport: oneTile });
|
||||||
|
karte.zoomTo(10);
|
||||||
|
karte.panTo({ easting: 50, northing: 50 });
|
||||||
|
karte.downloadView();
|
||||||
|
karte.startHessen();
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 0, total: 3 });
|
||||||
|
karte.setOnline(true);
|
||||||
|
await clock.tick();
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||||
|
karte.setOnline(false);
|
||||||
|
expect(appearance(karte, TILE_A)).toBe("drawn");
|
||||||
|
karte.panTo({ easting: 150, northing: 50 });
|
||||||
|
expect(appearance(karte, TILE_B)).toBe("blank-not-on-device");
|
||||||
|
karte.setOnline(true);
|
||||||
|
await clock.tick();
|
||||||
|
await clock.tick();
|
||||||
|
expect(karte.snapshot().visit).toBeNull();
|
||||||
|
expect(karte.snapshot().hessen).toEqual({ arrived: 3, total: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("closing drops the view fill, and downloadView again fetches what is still missing", async () => {
|
||||||
|
const opened = await openSheet({
|
||||||
|
viewport: oneTile,
|
||||||
|
uebersicht: {
|
||||||
|
tileSizePx: 10,
|
||||||
|
resolutions: [10, 20],
|
||||||
|
blaetter: [{ minE: 0, minN: 0, maxE: 200, maxN: 200 }],
|
||||||
|
gebiete: [{ name: "Hessen", covers: "all" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
opened.karte.zoomTo(20);
|
||||||
|
opened.karte.panTo({ easting: 100, northing: 100 });
|
||||||
|
opened.karte.downloadView();
|
||||||
|
expect(opened.karte.snapshot().visit).toEqual({ arrived: 0, total: 5 });
|
||||||
|
opened.karte.setOnline(true);
|
||||||
|
await opened.clock.tick();
|
||||||
|
opened.karte.setAppOpen(false);
|
||||||
|
expect(opened.karte.snapshot().visit).toBeNull();
|
||||||
|
await opened.clock.tick();
|
||||||
|
|
||||||
|
opened.karte.setAppOpen(true);
|
||||||
|
expect(opened.karte.snapshot().visit).toBeNull();
|
||||||
|
await opened.clock.tick();
|
||||||
|
await opened.clock.tick();
|
||||||
|
opened.karte.setOnline(false);
|
||||||
|
opened.karte.zoomTo(10);
|
||||||
|
opened.karte.panTo({ easting: 50, northing: 50 });
|
||||||
|
expect(appearance(opened.karte, "0/0/0")).toBe("blank-not-on-device");
|
||||||
|
|
||||||
|
opened.karte.zoomTo(20);
|
||||||
|
opened.karte.panTo({ easting: 100, northing: 100 });
|
||||||
|
opened.karte.setOnline(true);
|
||||||
|
opened.karte.downloadView();
|
||||||
|
expect(opened.karte.snapshot().visit).toEqual({ arrived: 0, total: 4 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deleteCache drops an unfinished view fill and does not start a fetch", async () => {
|
||||||
|
const { karte, clock } = await openSheet({ viewport: threeTiles });
|
||||||
|
karte.zoomTo(10);
|
||||||
|
karte.panTo({ easting: 150, northing: 50 });
|
||||||
|
karte.setOnline(true);
|
||||||
|
karte.downloadView();
|
||||||
|
karte.startHessen();
|
||||||
|
await clock.tick();
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||||
|
karte.deleteCache();
|
||||||
|
expect(karte.snapshot().visit).toBeNull();
|
||||||
|
expect(appearance(karte, TILE_A)).toBe("blank-not-on-device");
|
||||||
|
expect(appearance(karte, TILE_B)).toBe("blank-not-on-device");
|
||||||
|
await clock.tick();
|
||||||
|
expect(appearance(karte, TILE_B)).toBe("blank-not-on-device");
|
||||||
|
expect(karte.snapshot().visit).toBeNull();
|
||||||
|
expect(karte.snapshot().hessen).toEqual({ arrived: 0, total: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("losing the network pauses the view fill and regaining it continues", async () => {
|
||||||
|
const { karte, clock } = await openSheet({ viewport: threeTiles });
|
||||||
|
karte.zoomTo(10);
|
||||||
|
karte.panTo({ easting: 150, northing: 50 });
|
||||||
|
karte.downloadView();
|
||||||
|
karte.setOnline(true);
|
||||||
|
await clock.tick();
|
||||||
|
karte.setOnline(false);
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||||
|
karte.panTo({ easting: 50, northing: 50 });
|
||||||
|
await clock.tick();
|
||||||
|
expect(appearance(karte, TILE_B)).toBe("blank-not-on-device");
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 1, total: 3 });
|
||||||
|
karte.setOnline(true);
|
||||||
|
await clock.tick();
|
||||||
|
expect(karte.snapshot().visit).toEqual({ arrived: 2, total: 3 });
|
||||||
|
await clock.tick();
|
||||||
|
expect(karte.snapshot().visit).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+104
-8
@@ -1,5 +1,5 @@
|
|||||||
import { maxResolutionMeters } from "../config";
|
import { maxResolutionMeters } from "../config";
|
||||||
import { parseKoordinate } from "./koordinate";
|
import { parseKoordinate, writeKoordinate } from "./koordinate";
|
||||||
import type {
|
import type {
|
||||||
KachelView,
|
KachelView,
|
||||||
KarteOptions,
|
KarteOptions,
|
||||||
@@ -168,10 +168,12 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
|||||||
let personMovedView = false;
|
let personMovedView = false;
|
||||||
let personEnteredZiel = false;
|
let personEnteredZiel = false;
|
||||||
let ziele = restoreZiele(markState.ziele);
|
let ziele = restoreZiele(markState.ziele);
|
||||||
|
let downloadKnopf = markState.downloadKnopf === true;
|
||||||
let koordinateNotice: string | null = null;
|
let koordinateNotice: string | null = null;
|
||||||
let koordinate = { gitter: "32U MA", rechtswert: "", hochwert: "" };
|
let koordinate = { gitter: "32U MA", rechtswert: "", hochwert: "" };
|
||||||
let queue: string[] = [];
|
let queue: string[] = [];
|
||||||
let hessenFill: string[] | null = null;
|
let hessenFill: string[] | null = null;
|
||||||
|
let viewFill: string[] | null = null;
|
||||||
let fetchesHeld = false;
|
let fetchesHeld = false;
|
||||||
const inFlight = new Map<string, AbortController>();
|
const inFlight = new Map<string, AbortController>();
|
||||||
const failed = new Set<string>();
|
const failed = new Set<string>();
|
||||||
@@ -208,8 +210,12 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
|||||||
for (const listener of listeners) listener();
|
for (const listener of listeners) listener();
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistZiele(): void {
|
function persistMarks(): void {
|
||||||
void marks.write({ heruntergeladen: [], ziele: ziele.map((item) => item.koordinate) });
|
void marks.write({
|
||||||
|
heruntergeladen: [],
|
||||||
|
ziele: ziele.map((item) => item.koordinate),
|
||||||
|
downloadKnopf,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreZiele(canonical: string[]): { koordinate: string; point: Point }[] {
|
function restoreZiele(canonical: string[]): { koordinate: string; point: Point }[] {
|
||||||
@@ -282,6 +288,9 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
|||||||
if (hessenFill) {
|
if (hessenFill) {
|
||||||
for (const id of hessenFill) push(id);
|
for (const id of hessenFill) push(id);
|
||||||
}
|
}
|
||||||
|
if (viewFill) {
|
||||||
|
for (const id of viewFill) push(id);
|
||||||
|
}
|
||||||
queue = ordered;
|
queue = ordered;
|
||||||
pump();
|
pump();
|
||||||
}
|
}
|
||||||
@@ -305,6 +314,7 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
|||||||
stored.add(id);
|
stored.add(id);
|
||||||
await cache.put(id, bytes);
|
await cache.put(id, bytes);
|
||||||
if (hessenFill?.every((tile) => cached.has(tile))) hessenFill = null;
|
if (hessenFill?.every((tile) => cached.has(tile))) hessenFill = null;
|
||||||
|
if (viewFill?.every((tile) => cached.has(tile))) viewFill = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isAbort(error)) failed.add(id);
|
if (!isAbort(error)) failed.add(id);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -339,17 +349,48 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
|||||||
standort: standort ? { ...standort } : null,
|
standort: standort ? { ...standort } : null,
|
||||||
notice,
|
notice,
|
||||||
koordinate: { ...koordinate },
|
koordinate: { ...koordinate },
|
||||||
|
downloadKnopf,
|
||||||
hessen: { arrived: hessenArrived(), total: hessenIds.length },
|
hessen: { arrived: hessenArrived(), total: hessenIds.length },
|
||||||
visit: visitOf(),
|
visit: visitOf(),
|
||||||
kacheln: views,
|
kacheln: views,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visitIds(): string[] {
|
||||||
|
const ids: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const id of hessenFill ?? []) {
|
||||||
|
if (seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
for (const id of viewFill ?? []) {
|
||||||
|
if (seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
ids.push(id);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
function visitOf(): { arrived: number; total: number } | null {
|
function visitOf(): { arrived: number; total: number } | null {
|
||||||
if (!hessenFill) return null;
|
const ids = visitIds();
|
||||||
|
if (ids.length === 0) return null;
|
||||||
let arrived = 0;
|
let arrived = 0;
|
||||||
for (const id of hessenFill) if (cached.has(id)) arrived += 1;
|
for (const id of ids) if (cached.has(id)) arrived += 1;
|
||||||
return { arrived, total: hessenFill.length };
|
return { arrived, total: ids.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
function idsCovering(rect: Rect): string[] {
|
||||||
|
const ids: string[] = [];
|
||||||
|
const seen = 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 (seen.has(cell.id) || !catalog.has(cell.id)) continue;
|
||||||
|
seen.add(cell.id);
|
||||||
|
ids.push(cell.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
const session: KarteSession = {
|
const session: KarteSession = {
|
||||||
@@ -399,6 +440,7 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
|||||||
appOpen = open;
|
appOpen = open;
|
||||||
if (!open) {
|
if (!open) {
|
||||||
hessenFill = null;
|
hessenFill = null;
|
||||||
|
viewFill = null;
|
||||||
abortInFlight(() => true);
|
abortInFlight(() => true);
|
||||||
} else {
|
} else {
|
||||||
releaseFetches();
|
releaseFetches();
|
||||||
@@ -417,6 +459,43 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
|||||||
}
|
}
|
||||||
publish();
|
publish();
|
||||||
},
|
},
|
||||||
|
tap(point) {
|
||||||
|
const radius = 44 * resolution;
|
||||||
|
let nearest = -1;
|
||||||
|
let nearestDistance = Infinity;
|
||||||
|
for (let index = 0; index < ziele.length; index += 1) {
|
||||||
|
const distance = Math.hypot(
|
||||||
|
point.easting - ziele[index].point.easting,
|
||||||
|
point.northing - ziele[index].point.northing,
|
||||||
|
);
|
||||||
|
if (distance <= radius && distance < nearestDistance) {
|
||||||
|
nearest = index;
|
||||||
|
nearestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nearest >= 0) {
|
||||||
|
ziele = ziele.filter((_, index) => index !== nearest);
|
||||||
|
persistMarks();
|
||||||
|
publish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const written = writeKoordinate(point);
|
||||||
|
if (!written) return;
|
||||||
|
const parsed = parseKoordinate(written, (candidate) =>
|
||||||
|
uebersicht.blaetter.some((blatt) => coversPoint(blatt, candidate)),
|
||||||
|
);
|
||||||
|
if (!parsed.ok) return;
|
||||||
|
if (ziele.some((item) => item.koordinate === parsed.koordinate)) {
|
||||||
|
koordinateNotice = "Diese Koordinate ist schon ein Ziel.";
|
||||||
|
publish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
koordinateNotice = null;
|
||||||
|
ziele = [{ koordinate: parsed.koordinate, point: parsed.point }, ...ziele];
|
||||||
|
personEnteredZiel = true;
|
||||||
|
persistMarks();
|
||||||
|
publish();
|
||||||
|
},
|
||||||
enterKoordinate(input) {
|
enterKoordinate(input) {
|
||||||
const parsed = parseKoordinate(input, (point) =>
|
const parsed = parseKoordinate(input, (point) =>
|
||||||
uebersicht.blaetter.some((blatt) => coversPoint(blatt, point)),
|
uebersicht.blaetter.some((blatt) => coversPoint(blatt, point)),
|
||||||
@@ -442,7 +521,7 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
|||||||
releaseFetches();
|
releaseFetches();
|
||||||
center = { ...parsed.point };
|
center = { ...parsed.point };
|
||||||
resolution = clamp(config.openResolutionMeters);
|
resolution = clamp(config.openResolutionMeters);
|
||||||
persistZiele();
|
persistMarks();
|
||||||
rebuild();
|
rebuild();
|
||||||
publish();
|
publish();
|
||||||
},
|
},
|
||||||
@@ -450,7 +529,12 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
|||||||
const next = ziele.filter((item) => item.koordinate !== koordinate);
|
const next = ziele.filter((item) => item.koordinate !== koordinate);
|
||||||
if (next.length === ziele.length) return;
|
if (next.length === ziele.length) return;
|
||||||
ziele = next;
|
ziele = next;
|
||||||
persistZiele();
|
persistMarks();
|
||||||
|
publish();
|
||||||
|
},
|
||||||
|
setDownloadKnopf(on) {
|
||||||
|
downloadKnopf = on;
|
||||||
|
persistMarks();
|
||||||
publish();
|
publish();
|
||||||
},
|
},
|
||||||
centerOnStandort() {
|
centerOnStandort() {
|
||||||
@@ -481,9 +565,21 @@ export async function openKarte(options: KarteOptions): Promise<KarteSession> {
|
|||||||
rebuild();
|
rebuild();
|
||||||
publish();
|
publish();
|
||||||
},
|
},
|
||||||
|
downloadView() {
|
||||||
|
if (viewFill) return;
|
||||||
|
const missing = idsCovering(viewRect(center, resolution, viewport)).filter(
|
||||||
|
(id) => !cached.has(id),
|
||||||
|
);
|
||||||
|
if (missing.length === 0) return;
|
||||||
|
releaseFetches();
|
||||||
|
viewFill = missing;
|
||||||
|
rebuild();
|
||||||
|
publish();
|
||||||
|
},
|
||||||
deleteCache() {
|
deleteCache() {
|
||||||
fetchesHeld = true;
|
fetchesHeld = true;
|
||||||
hessenFill = null;
|
hessenFill = null;
|
||||||
|
viewFill = null;
|
||||||
abortInFlight(() => true);
|
abortInFlight(() => true);
|
||||||
const ids = [...stored];
|
const ids = [...stored];
|
||||||
cached.clear();
|
cached.clear();
|
||||||
|
|||||||
+13
-2
@@ -58,6 +58,7 @@ export type KarteSnapshot = {
|
|||||||
standort: Point | null;
|
standort: Point | null;
|
||||||
notice: string | null;
|
notice: string | null;
|
||||||
koordinate: KoordinateText;
|
koordinate: KoordinateText;
|
||||||
|
downloadKnopf: boolean;
|
||||||
hessen: { arrived: number; total: number };
|
hessen: { arrived: number; total: number };
|
||||||
visit: { arrived: number; total: number } | null;
|
visit: { arrived: number; total: number } | null;
|
||||||
kacheln: KachelView[];
|
kacheln: KachelView[];
|
||||||
@@ -69,9 +70,16 @@ export interface CachePort {
|
|||||||
delete(ids: string[]): Promise<void>;
|
delete(ids: string[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Device memory for Ziele and the Download-Knopf. `heruntergeladen` is only how an older value is read. */
|
||||||
|
export type RememberedMarks = {
|
||||||
|
heruntergeladen: string[];
|
||||||
|
ziele: string[];
|
||||||
|
downloadKnopf: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export interface MarksPort {
|
export interface MarksPort {
|
||||||
read(): Promise<{ heruntergeladen: string[]; ziele: string[] }>;
|
read(): Promise<RememberedMarks>;
|
||||||
write(state: { heruntergeladen: string[]; ziele: string[] }): Promise<void>;
|
write(state: RememberedMarks): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fetches Kachel bytes. The session decides when, and stores them in the Cache. */
|
/** Fetches Kachel bytes. The session decides when, and stores them in the Cache. */
|
||||||
@@ -102,9 +110,12 @@ export type KarteSession = {
|
|||||||
setAppOpen(open: boolean): void;
|
setAppOpen(open: boolean): void;
|
||||||
setOnline(online: boolean): void;
|
setOnline(online: boolean): void;
|
||||||
enterKoordinate(input: KoordinateText): void;
|
enterKoordinate(input: KoordinateText): void;
|
||||||
|
tap(point: Point): void;
|
||||||
deleteZiel(koordinate: string): void;
|
deleteZiel(koordinate: string): void;
|
||||||
centerOnStandort(): void;
|
centerOnStandort(): void;
|
||||||
centerOnZiel(koordinate: string): void;
|
centerOnZiel(koordinate: string): void;
|
||||||
startHessen(): void;
|
startHessen(): void;
|
||||||
|
setDownloadKnopf(on: boolean): void;
|
||||||
|
downloadView(): void;
|
||||||
deleteCache(): void;
|
deleteCache(): void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ input {
|
|||||||
background: #e7e1d4;
|
background: #e7e1d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-download {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
right: max(12px, env(safe-area-inset-right));
|
||||||
|
bottom: max(12px, env(safe-area-inset-bottom));
|
||||||
|
max-width: calc(100% - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
.map .ol-viewport {
|
.map .ol-viewport {
|
||||||
background: #e7e1d4;
|
background: #e7e1d4;
|
||||||
}
|
}
|
||||||
@@ -123,6 +131,23 @@ label {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-knopf {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-knopf input {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
min-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
accent-color: #1e3328;
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
|
|||||||
@@ -217,6 +217,10 @@ export function createFieldMap(
|
|||||||
pushView();
|
pushView();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
map.on("singleclick", (event) => {
|
||||||
|
const [easting, northing] = event.coordinate;
|
||||||
|
session.tap({ easting, northing });
|
||||||
|
});
|
||||||
map.on("pointerdrag", rememberGesture);
|
map.on("pointerdrag", rememberGesture);
|
||||||
map.getViewport()?.addEventListener("wheel", rememberGesture, { passive: true });
|
map.getViewport()?.addEventListener("wheel", rememberGesture, { passive: true });
|
||||||
map.on("moveend", () => {
|
map.on("moveend", () => {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export function mountScreen(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="einstellungen" class="fold" hidden>
|
<div id="einstellungen" class="fold" hidden>
|
||||||
|
<label class="download-knopf"><input type="checkbox" /> Download-Knopf</label>
|
||||||
<div class="gebiet">
|
<div class="gebiet">
|
||||||
<p hidden></p>
|
<p hidden></p>
|
||||||
<button type="button" class="hessen-download">Hessen herunterladen</button>
|
<button type="button" class="hessen-download">Hessen herunterladen</button>
|
||||||
@@ -67,6 +68,7 @@ export function mountScreen(
|
|||||||
const hochwert = panel.querySelector<HTMLInputElement>('input[name="hochwert"]');
|
const hochwert = panel.querySelector<HTMLInputElement>('input[name="hochwert"]');
|
||||||
const zielList = panel.querySelector<HTMLElement>(".ziel-list");
|
const zielList = panel.querySelector<HTMLElement>(".ziel-list");
|
||||||
const standortButton = panel.querySelector<HTMLButtonElement>(".actions button");
|
const standortButton = panel.querySelector<HTMLButtonElement>(".actions button");
|
||||||
|
const downloadKnopf = panel.querySelector<HTMLInputElement>(".download-knopf input");
|
||||||
const gebietBlock = panel.querySelector<HTMLElement>(".gebiet");
|
const gebietBlock = panel.querySelector<HTMLElement>(".gebiet");
|
||||||
const gebietText = panel.querySelector<HTMLElement>(".gebiet p");
|
const gebietText = panel.querySelector<HTMLElement>(".gebiet p");
|
||||||
const gebietButton = panel.querySelector<HTMLButtonElement>(".gebiet button");
|
const gebietButton = panel.querySelector<HTMLButtonElement>(".gebiet button");
|
||||||
@@ -88,6 +90,7 @@ export function mountScreen(
|
|||||||
!hochwert ||
|
!hochwert ||
|
||||||
!zielList ||
|
!zielList ||
|
||||||
!standortButton ||
|
!standortButton ||
|
||||||
|
!downloadKnopf ||
|
||||||
!gebietBlock ||
|
!gebietBlock ||
|
||||||
!gebietText ||
|
!gebietText ||
|
||||||
!gebietButton ||
|
!gebietButton ||
|
||||||
@@ -107,6 +110,11 @@ export function mountScreen(
|
|||||||
hochwert.value = opening.koordinate.hochwert;
|
hochwert.value = opening.koordinate.hochwert;
|
||||||
|
|
||||||
const fieldMap = createFieldMap(mapElement, uebersicht, session);
|
const fieldMap = createFieldMap(mapElement, uebersicht, session);
|
||||||
|
const viewDownload = document.createElement("button");
|
||||||
|
viewDownload.type = "button";
|
||||||
|
viewDownload.className = "map-download";
|
||||||
|
viewDownload.textContent = "Diese Ansicht herunterladen";
|
||||||
|
viewDownload.addEventListener("click", () => session.downloadView());
|
||||||
let pendingWipe = false;
|
let pendingWipe = false;
|
||||||
let kachelKey = kachelnOf(session.snapshot());
|
let kachelKey = kachelnOf(session.snapshot());
|
||||||
|
|
||||||
@@ -133,6 +141,9 @@ export function mountScreen(
|
|||||||
});
|
});
|
||||||
|
|
||||||
standortButton.addEventListener("click", () => session.centerOnStandort());
|
standortButton.addEventListener("click", () => session.centerOnStandort());
|
||||||
|
downloadKnopf.addEventListener("change", () => {
|
||||||
|
session.setDownloadKnopf(downloadKnopf.checked);
|
||||||
|
});
|
||||||
gebietButton.addEventListener("click", () => {
|
gebietButton.addEventListener("click", () => {
|
||||||
if (gebietButton.disabled) return;
|
if (gebietButton.disabled) return;
|
||||||
session.startHessen();
|
session.startHessen();
|
||||||
@@ -192,6 +203,10 @@ export function mountScreen(
|
|||||||
notice.textContent = "";
|
notice.textContent = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadKnopf.checked = snapshot.downloadKnopf;
|
||||||
|
if (snapshot.downloadKnopf) mapElement.append(viewDownload);
|
||||||
|
else viewDownload.remove();
|
||||||
|
|
||||||
const missing = snapshot.hessen.arrived < snapshot.hessen.total;
|
const missing = snapshot.hessen.arrived < snapshot.hessen.total;
|
||||||
gebietBlock.hidden = false;
|
gebietBlock.hidden = false;
|
||||||
gebietButton.textContent = "Hessen herunterladen";
|
gebietButton.textContent = "Hessen herunterladen";
|
||||||
|
|||||||
Reference in New Issue
Block a user