import { Throttler } from "./Throttler"; import { setTopbarOffset, addToggleDropdownListener } from "./topbar"; import { Coordinate, Position, convertPixelsToCoordinate, convertCoordinateToPixels } from "./coordinates"; import { Size } from "./size"; import { Tooltip } from "./Tooltip"; import { loadReviews } from "./review"; const domSelect = (query: string) => document.querySelector(query); const tooltip = new Tooltip(document.getElementById("tooltip")!); type Square = { x1: number, y1: number, x2: number, y2: number, }; type ZipCodeReverseResponse = { nr: number | null; navn: string; visueltcenter: number[]; bbox: number[]; }; async function fetchZipCode({ longitude, latitude, }: Coordinate): Promise { return fetch( `https://api.dataforsyningen.dk/postnumre/reverse?x=${longitude}&y=${latitude}&landpostnumre`, ) .then((request) => request.json()) .then((data) => { let zipCode = parseInt(data.nr); return { ...data, nr: isNaN(zipCode) ? null : zipCode, } as ZipCodeReverseResponse; }) .catch(() => null as never); } let currentBoundary: Array | null = null; async function fetchAndDisplayZipCode(coords: Coordinate) { if (currentBoundary && coords.longitude > currentBoundary[0] && coords.latitude > currentBoundary[1] && coords.longitude < currentBoundary[2] && coords.latitude < currentBoundary[3] ) return; const response = await fetchZipCode(coords); currentBoundary = response.bbox; displayZipCode( domSelect("#zip-code")!, response.nr, response.navn, response.visueltcenter ? { longitude: response.visueltcenter[0], latitude: response.visueltcenter[1] } : null, response.bbox ? { x1: response.bbox[0], y1: response.bbox[1], x2: response.bbox[2], y2: response.bbox[3] } : null, ); } function displayMousePosition(element: HTMLParagraphElement, mouse: Position) { element.innerHTML = `Mouse position: (${mouse.x}px, ${mouse.y}px)`; } function displayCoords(element: HTMLParagraphElement, coords: Coordinate) { const longitude = coords.longitude.toFixed(3); const latitude = coords.latitude.toFixed(3); element.innerHTML = `Coords: ${longitude}, ${latitude}`; } function displayZipCode( element: HTMLParagraphElement, zipCode: number | null, name: string | null, center: Coordinate | null, boundary: Square | null, ) { element.innerHTML = zipCode === null ? `Postnummer ikke fundet` : `Postnummer: ${zipCode}, ${name}`; tooltip.setText(zipCode ? `${zipCode} ${name}` : ""); const dot = document.getElementById("dot")!; const boundaryElem = document.getElementById("boundary")!; if (!center || !boundary) { dot.style.display = "none"; boundaryElem.style.display = "none"; return; } const mapImg = document.getElementById("map")!; const mapSize: Size = { width: mapImg.clientWidth, height: mapImg.clientHeight, }; // Draw dot const position = convertCoordinateToPixels(center, mapSize); const rect = document.getElementById("map")!.getBoundingClientRect(); dot.style.display = "block"; dot.style.left = `${position.x + rect.left}px`; dot.style.top = `${position.y + rect.top + document.documentElement.scrollTop}px`; // Draw boundary const bottomleft = convertCoordinateToPixels({ longitude: boundary.x1, latitude: boundary.y1 }, mapSize); const topright = convertCoordinateToPixels({ longitude: boundary.x2, latitude: boundary.y2 }, mapSize); boundaryElem.style.display = "block"; boundaryElem.style.left = `${bottomleft.x + rect.left}px`; boundaryElem.style.top = `${topright.y + rect.top + document.documentElement.scrollTop}px`; boundaryElem.style.width = `${topright.x - bottomleft.x}px`; boundaryElem.style.height = `${bottomleft.y - topright.y}px`; } type Component = { children?: Component[], render(): string, hydrate?(update: (action?: () => Promise) => Promise): Promise, }; function makeReviewsPage(): Component { return { render: () => /*html*/`

Anmeldelser

${loadReviews()}
`, } } function makeSearchBar(updateZipCodeInfo: (zipCode: string) => Promise): Component { return { render: () => /*html*/` `, async hydrate(update) { const searchBar = domSelect("#search-bar")!; const searchInput = domSelect("#search-input")!; searchInput.addEventListener("keydown", (event) => { if (event.key.length === 1 && !"0123456789".includes(event.key)) event.preventDefault(); }); searchBar.addEventListener("submit", async (event: Event) => { event.preventDefault(); update(() => updateZipCodeInfo(searchInput.value)); }); }, }; } type ZipCodeInfo = { zipCode: number | null, name: string | null, center: Coordinate | null, boundary: Square | null, } function makeInfoBox(info: ZipCodeInfo): Component { const { zipCode, name, center, boundary } = info; return { render: () => /*html*/`

${info.zipCode === null ? "Postnummer ikke fundet" : `Postnummer: ${zipCode}, ${name}`}

`, async hydrate() { tooltip.setText(zipCode ? `${zipCode} ${name}` : ""); const dot = document.getElementById("dot")!; const boundaryElem = document.getElementById("boundary")!; if (!center || !boundary) { dot.style.display = "none"; boundaryElem.style.display = "none"; return; } const mapImg = document.getElementById("map")!; const mapSize: Size = { width: mapImg.clientWidth, height: mapImg.clientHeight, }; // Draw dot const position = convertCoordinateToPixels(center, mapSize); const rect = document.getElementById("map")!.getBoundingClientRect(); dot.style.display = "block"; dot.style.left = `${position.x + rect.left}px`; dot.style.top = `${position.y + rect.top + document.documentElement.scrollTop}px`; // Draw boundary const bottomleft = convertCoordinateToPixels({ longitude: boundary.x1, latitude: boundary.y1 }, mapSize); const topright = convertCoordinateToPixels({ longitude: boundary.x2, latitude: boundary.y2 }, mapSize); boundaryElem.style.display = "block"; boundaryElem.style.left = `${bottomleft.x + rect.left}px`; boundaryElem.style.top = `${topright.y + rect.top + document.documentElement.scrollTop}px`; boundaryElem.style.width = `${topright.x - bottomleft.x}px`; boundaryElem.style.height = `${bottomleft.y - topright.y}px`; } } } function makeMainPage(): Component { const zipCodeInfo: ZipCodeInfo = { zipCode: null, name: null, center: null, boundary: null, } const searchBar = makeSearchBar(async (searchInputValue) => { const inputValue = searchInputValue; if (!/^\d+$/.test(inputValue)) return; const data = await fetch( `https://api.dataforsyningen.dk/postnumre?nr=${inputValue}`, ).then((response) => response.json()) zipCodeInfo.zipCode = data.length ? parseInt(data[0]["nr"]) : null; zipCodeInfo.name = data.length ? data[0]["navn"] : null; zipCodeInfo.center = data.length ? { longitude: data[0]["visueltcenter"][0], latitude: data[0]["visueltcenter"][1] } : null; zipCodeInfo.boundary = data.length ? { x1: data[0]["bbox"][0], y1: data[0]["bbox"][1], x2: data[0]["bbox"][2], y2: data[0]["bbox"][3] } : null; }); const infoBox = makeInfoBox(zipCodeInfo); return { children: [searchBar, infoBox], render: () => /*html*/` ${searchBar.render()}
`, async hydrate() { const mousePositionElement = domSelect("#mouse-position")!; const coordsElement = domSelect("#coords")!; const zipCodeElement = domSelect("#zip-code")!; const mapImg = domSelect("#map")!; const fetcher = new Throttler(200); mapImg.addEventListener('mousemove', async (event: MouseEvent) => { const mousePosition: Position = { x: event.offsetX, y: event.offsetY }; displayMousePosition(mousePositionElement, mousePosition); const mapSize: Size = { width: mapImg.clientWidth, height: mapImg.clientHeight, }; const coords = convertPixelsToCoordinate(mousePosition, mapSize); displayCoords(coordsElement, coords); fetcher.call(async () => await fetchAndDisplayZipCode(coords)); }); mapImg.addEventListener("mouseup", async (event: MouseEvent) => { const mousePosition: Position = { x: event.offsetX, y: event.offsetY }; displayMousePosition(mousePositionElement, mousePosition); const mapSize: Size = { width: mapImg.clientWidth, height: mapImg.clientHeight, }; const coords = convertPixelsToCoordinate(mousePosition, mapSize); displayCoords(coordsElement, coords); fetcher.call(async () => await fetchAndDisplayZipCode(coords)); }); mapImg.addEventListener("mouseleave", (_event: MouseEvent) => { displayZipCode(zipCodeElement, null, null, null, null); }); }, }; } function makeRouter(route: "main" | "reviews"): Component { switch (route) { case "main": { const mainPage = makeMainPage(); return { children: [mainPage], render: () => mainPage.render(), }; } case "reviews": { const reviewsPage = makeReviewsPage(); return { children: [reviewsPage], render: () => reviewsPage.render(), } } } } function makeTooltip(): Component { return { render: () => /*html*/`
`, } } function makeLayout(): Component { let route: "main" | "reviews" = "main"; const router = makeRouter(route); const tooltip = makeTooltip(); return { children: [router, tooltip], render: () => /**/`

Postnummer App

${router.render()}
${tooltip.render()} `, async hydrate(update) { setTopbarOffset(); addToggleDropdownListener(); document.getElementById("reviews-redirect")!.addEventListener('click', () => { update(async () => route = "reviews"); }) document.getElementById("map-redirect")!.addEventListener('click', () => { update(async () => route = "main"); }) }, } } function hydrateComponentAndChildren(component: Component, update: (action?: () => Promise) => Promise) { component.hydrate && component.hydrate(update); component.children?.forEach((child) => hydrateComponentAndChildren(child, update)); } function renderComponent(element: HTMLElement, component: Component) { element.innerHTML = component.render(); window.requestAnimationFrame(() => { hydrateComponentAndChildren(component, async (action) => { action && await action(); element.innerHTML = component.render(); }); }); } function main() { if (navigator.userAgent.match("Chrome")) { location.href = "https://mozilla.org/firefox"; } const bodyElement = document.querySelector("body")!; renderComponent(bodyElement, makeLayout()); } main();