diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-07 10:35:28 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-07 10:35:28 +0200 |
| commit | 08528a9e05a12e564f2c3776be4c8ae672f5054c (patch) | |
| tree | b5fe872701d0e30b5df18f7d4ea994b5b4a1761a | |
| parent | f3d3700dcca8555da9882f923a9fc4a30fcab3b8 (diff) | |
| download | housing-08528a9e05a12e564f2c3776be4c8ae672f5054c.tar.zst | |
Minor dom cleanup
| -rw-r--r-- | app/dom.js | 81 | ||||
| -rw-r--r-- | app/geometry.js | 16 | ||||
| -rw-r--r-- | app/main.js | 91 | ||||
| -rw-r--r-- | app/map.js | 6 | ||||
| -rw-r--r-- | app/models.js | 44 | ||||
| -rw-r--r-- | app/svg.js | 8 |
6 files changed, 85 insertions, 161 deletions
@@ -44,7 +44,7 @@ export class Dom { for (const cls of classes) div.classList.add(cls); if (textContent) div.textContent = textContent; for (const [k, v] of Object.entries(attributes)) div.setAttribute(k, v); - Dom.appendChildren(div, children); + if (children) div.append(...children); return div; } @@ -69,7 +69,7 @@ export class Dom { for (const cls of classes) span.classList.add(cls); if (textContent) span.textContent = textContent; for (const [k, v] of Object.entries(attributes)) span.setAttribute(k, v); - Dom.appendChildren(span, children); + if (children) span.append(...children); return span; } @@ -95,7 +95,7 @@ export class Dom { for (const cls of classes) button.classList.add(cls); if (textContent) button.textContent = textContent; for (const [k, v] of Object.entries(attributes)) button.setAttribute(k, v); - Dom.appendChildren(button, children); + if (children) button.append(...children); if (onClick) button.addEventListener("click", onClick); return button; } @@ -180,7 +180,7 @@ export class Dom { for (const cls of classes) label.classList.add(cls); if (textContent) label.textContent = textContent; for (const [k, v] of Object.entries(attributes)) label.setAttribute(k, v); - Dom.appendChildren(label, children); + if (children) label.append(...children); label.htmlFor = htmlFor; return label; } @@ -211,7 +211,7 @@ export class Dom { for (const cls of classes) heading.classList.add(cls); if (textContent) heading.textContent = textContent; for (const [k, v] of Object.entries(attributes)) heading.setAttribute(k, v); - Dom.appendChildren(heading, children); + if (children) heading.append(...children); return heading; } @@ -260,7 +260,7 @@ export class Dom { if (id) select.id = id; for (const cls of classes) select.classList.add(cls); for (const [k, v] of Object.entries(attributes)) select.setAttribute(k, v); - Dom.appendChildren(select, children); + if (children) select.append(...children); if (onChange) select.addEventListener("change", onChange); return select; } @@ -301,7 +301,7 @@ export class Dom { for (const cls of classes) p.classList.add(cls); if (textContent) p.textContent = textContent; for (const [k, v] of Object.entries(attributes)) p.setAttribute(k, v); - Dom.appendChildren(p, children); + if (children) p.append(...children); return p; } @@ -380,7 +380,7 @@ export class Dom { }, textContent: `Score: ${house.scores.current.toFixed(1)}`, }); - Dom.appendChildren(header, [title, score]); + header.append(title, score); frag.appendChild(header); /* Details grid */ @@ -393,12 +393,12 @@ export class Dom { }, }); const details = [ - { label: "Price", value: `${house.property.price} €` }, - { label: "Building Type", value: house.property.buildingType }, - { label: "Construction Year", value: house.property.constructionYear?.toString() ?? "N/A" }, - { label: "Living Area", value: `${house.property.livingArea} m²` }, - { label: "District", value: house.location.district }, - { label: "Rooms", value: house.property.rooms?.toString() ?? "N/A" }, + { label: "Price", value: `${house.price} €` }, + { label: "Building Type", value: house.buildingType }, + { label: "Construction Year", value: house.constructionYear?.toString() ?? "N/A" }, + { label: "Living Area", value: `${house.livingArea} m²` }, + { label: "District", value: house.district }, + { label: "Rooms", value: house.rooms?.toString() ?? "N/A" }, { label: "Price", value: `${house.price} €` }, { label: "Building Type", value: house.buildingType }, { label: "Construction Year", value: house.constructionYear?.toString() ?? "N/A" }, @@ -430,7 +430,7 @@ export class Dom { styles: { color: "#333", fontSize: "14px", lineHeight: "1.4", marginTop: "5px" }, textContent: house.description ?? "No description available.", }); - Dom.appendChildren(descSect, [descTitle, descText]); + descSect.append(descTitle, descText); frag.appendChild(descSect); /* Images */ @@ -452,7 +452,7 @@ export class Dom { }), ); } - Dom.appendChildren(imgSect, [imgTitle, imgCont]); + imgSect.append(imgTitle, imgCont); frag.appendChild(imgSect); } @@ -503,35 +503,6 @@ export class Dom { } /** - * Loading spinner - * @returns {HTMLDivElement} - */ - static loadingIndicator() { - const spinner = Dom.div({ - classes: ["loading-indicator"], - styles: { - animation: "spin 1s linear infinite", - border: "2px solid #f3f3f3", - borderRadius: "50%", - borderTop: "2px solid #3498db", - display: "inline-block", - height: "16px", - width: "16px", - }, - }); - - if (!document.querySelector("#loading-styles")) { - const style = document.createElement("style"); - style.id = "loading-styles"; - style.textContent = ` - @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } - `; - document.head.appendChild(style); - } - return spinner; - } - - /** * Remove all children * @param {HTMLElement} el */ @@ -540,26 +511,6 @@ export class Dom { } /** - * Append many children - * @param {HTMLElement} parent - * @param {HTMLElement[]} children - */ - static appendChildren(parent, children) { - for (const child of children) { - if (child) parent.appendChild(child); - } - } - - /** - * Replace an element - * @param {HTMLElement} oldEl - * @param {HTMLElement} newEl - */ - static replace(oldEl, newEl) { - oldEl.parentNode?.replaceChild(newEl, oldEl); - } - - /** * Create a weight slider * @param {string} id * @param {string} labelText diff --git a/app/geometry.js b/app/geometry.js index b9cac4f..9b89106 100644 --- a/app/geometry.js +++ b/app/geometry.js @@ -134,10 +134,9 @@ export class Geometry { /** * Deserialize geometry from GeoJSON * @param {{type: string, coordinates?: any, [key: string]: any}} geojson - * @returns {Geometry|null} + * @returns {Point|LineString|Polygon|MultiLineString} */ static fromGeoJSON(geojson) { - if (!geojson?.type) return null; switch (geojson.type) { case "Point": return Point.fromGeoJSON(geojson); @@ -154,7 +153,7 @@ export class Geometry { /** * Compute geometry bounds - * @param {Geometry} geometry - Input geometry + * @param {Point|LineString|Polygon} geometry - Input geometry * @returns {Bounds} Geometry bounds */ static bounds(geometry) { @@ -169,8 +168,7 @@ export class Geometry { if (geometry instanceof Polygon) { return Geometry.calculateBoundsFromCoords(geometry.rings[0]); } - - throw new Error(`Unsupported geometry type: ${geometry.type}`); + throw new Error(`Unsupported geometry type: ${geometry}`); } /** @@ -1057,10 +1055,6 @@ export class Feature { * @param {Object} geojson - GeoJSON feature * @returns {Feature|null} */ - /** - * @param {{geometry: object, properties?: object, id?: string|number}} geojson - * @returns {Feature|null} - */ static fromGeoJSON(geojson) { if (!geojson?.geometry) return null; @@ -1073,7 +1067,7 @@ export class Feature { export class Collection { /** - * @param {Feature[]} [features=[]] - Feature array + * @param {Feature[]} features - Feature array */ constructor(features = []) { this.type = "FeatureCollection"; @@ -1101,7 +1095,7 @@ export class Collection { * @returns {Collection} */ static fromGeoJSON(geojson) { - const features = geojson.features ?? []; + const features = geojson.features.map(Feature.fromGeoJSON).filter((x) => x !== null); return new Collection(features); } diff --git a/app/main.js b/app/main.js index 5bd3534..6f5f7b7 100644 --- a/app/main.js +++ b/app/main.js @@ -34,8 +34,8 @@ export class App { #controls; /** @type {HTMLDialogElement|null} */ #modal = null; - /** @type {number | null} */ - #modalTimer = null; + /** @type {number | undefined} */ + #modalTimer = undefined; /** @type {boolean} */ #persistent = false; /** @type {string} */ @@ -51,38 +51,7 @@ export class App { margin: "0", }); - const loading = App.createLoading(); - - // Create main content container - const mainContainer = Dom.div({ - styles: { - display: "flex", - flex: "1", - overflow: "hidden", - }, - }); - - // Create map container - const mapContainer = Dom.div({ - styles: { - display: "flex", - flex: "1", - flexDirection: "column", - minWidth: "0", // Prevents flex overflow - }, - }); - - const stats = Dom.div({ - styles: { - background: "#fff", - borderTop: "1px solid #ddd", - flexShrink: "0", - fontSize: "0.95rem", - padding: "0.75rem 1rem", - }, - }); - - const controls = App.buildControls( + this.#controls = App.buildControls( this.#filters, this.#weights, () => this.#applyFilters(), @@ -100,13 +69,6 @@ export class App { }, ); - // Build layout hierarchy - mainContainer.append(controls, mapContainer); - document.body.append(loading, mainContainer); - - this.#stats = stats; - this.#controls = controls; - this.#map = new MapEl({ onHouseClick: (houseId, persistent) => this.#showHouseModal(houseId, persistent), onHouseHover: (houseId, hide) => { @@ -118,16 +80,19 @@ export class App { }, }); - mapContainer.append(this.#map.svg, stats); - this.#initialize(loading); - } + this.#stats = Dom.div({ + id: "stats", + styles: { + background: "#fff", + borderTop: "1px solid #ddd", + flexShrink: "0", + fontSize: "0.95rem", + padding: "0.75rem 1rem", + }, + }); - /** - * Create loading indicator - * @returns {HTMLElement} - */ - static createLoading() { - return Dom.div({ + const loading = Dom.div({ + id: "loading", styles: { background: "white", borderRadius: "8px", @@ -144,6 +109,32 @@ export class App { }, textContent: "Loading data…", }); + + document.body.append( + loading, + Dom.div({ + children: [ + this.#controls, + Dom.div({ + children: [this.#map.svg, this.#stats], + id: "map-container", + styles: { + display: "flex", + flex: "1", + flexDirection: "column", + minWidth: "0", // Prevents flex overflow + }, + }), + ], + id: "main", + styles: { + display: "flex", + flex: "1", + overflow: "hidden", + }, + }), + ); + this.#initialize(loading); } /** @@ -292,14 +292,14 @@ export class MapEl { if (feature.geometry instanceof LineString) { return Svg.path( - feature.geometry, + feature.geometry.simplify(30), new SvgOptions({ attributes: defaultStyle, }), ); } else if (feature.geometry instanceof MultiLineString) { return Svg.path( - new LineString(feature.geometry.coordinates.flat()), + new LineString(feature.geometry.simplify(30).coordinates.flat()), new SvgOptions({ attributes: defaultStyle, }), @@ -473,7 +473,7 @@ export class MapEl { static #getDistricts(districts) { return districts.map((district) => { const poly = Svg.polygon( - district.polygon, + district.polygon.simplify(30), new SvgOptions({ attributes: { "data-id": district.name, diff --git a/app/models.js b/app/models.js index d052f1c..1f39192 100644 --- a/app/models.js +++ b/app/models.js @@ -175,7 +175,7 @@ export class District { const name = "nimi_fi" in feature.properties ? feature.properties.nimi_fi : ""; const geometry = feature.geometry; - if (name === undefined || !(geometry instanceof Polygon)) { + if (name === null || name === undefined || !(geometry instanceof Polygon)) { throw new Error("Invalid district feature data"); } @@ -235,11 +235,10 @@ export class TrainTracks { * @returns {TrainTracks} */ static fromFeature(feature) { - const geometry = feature.geometry; - if (!(geometry instanceof LineString)) { - throw new Error("Invalid train tracks feature data"); + if (!(feature.geometry instanceof LineString)) { + throw new Error("Invalid train tracks feature data {feature}"); } - return new TrainTracks(geometry); + return new TrainTracks(feature.geometry); } /** @@ -268,12 +267,11 @@ export class TrainStation { * @returns {TrainStation|null} */ static fromFeature(feature) { - const geometry = feature.geometry; - if (!(geometry instanceof Polygon)) { - console.warn(`Train station feature ${JSON.stringify(feature)} is not a Polygon`); - return null; + if (!(feature.geometry instanceof Polygon)) { + throw new Error("Invalid train stations feature data {feature}"); } - return new TrainStation(geometry); + + return new TrainStation(feature.geometry); } /** @@ -291,9 +289,9 @@ export class House { * @param {string} id * @param {string} address * @param {string} district - * @param {Point} coordinates - Use Point geometry instead of GeoPoint - * @param {string} [postalCode] + * @param {Point} coordinates * @param {number} price + * @param {string} [postalCode] * @param {string} buildingType * @param {number} [constructionYear] * @param {number} [rooms] @@ -303,9 +301,9 @@ export class House { * @param {string} [condition] * @param {string} [description] * @param {PriceUpdate[]} [priceHistory] - * @param {Date} firstSeen - * @param {Date} lastSeen - * @param {Date} lastUpdated + * @param {Date|null} firstSeen + * @param {Date|null} lastSeen + * @param {Date|null} lastUpdated * @param {Date|null} [disappeared] * @param {Scores} scores * @param {string[]} images @@ -316,9 +314,9 @@ export class House { address, district, coordinates, - postalCode = "", price, - buildingType, + postalCode = "", + buildingType = "", constructionYear = 0, rooms = 0, livingArea = 0, @@ -327,9 +325,9 @@ export class House { condition = "", description = "", priceHistory = [], - firstSeen, - lastSeen, - lastUpdated, + firstSeen = null, + lastSeen = null, + lastUpdated = null, disappeared = null, scores = new Scores(0), images = [], @@ -339,8 +337,8 @@ export class House { this.address = address; this.district = district; this.coordinates = coordinates; - this.postalCode = postalCode; this.price = price; + this.postalCode = postalCode; this.buildingType = buildingType; this.constructionYear = constructionYear; this.rooms = rooms; @@ -390,8 +388,8 @@ export class House { rawLocation.address || "", rawLocation.district || "", coordinates, - rawLocation.zipCode || "", parsePrice(rawData.price), + rawLocation.zipCode || "", House.getBuildingType(data.type), rawData.buildYear || 0, rawData.rooms || 0, @@ -546,8 +544,6 @@ export class DataProvider { } const data = /** @type {ApiResponse} */ (await response.json()); - - // Extract the doc objects from each row and filter out any null/undefined const housesData = data.rows.map((row) => row.doc).filter((doc) => doc?.raw?.location); console.log(`Loaded ${housesData.length} houses from API`); @@ -1,13 +1,5 @@ import { LineString, Point, Polygon } from "geom"; -/** - * Class representing options for SVG elements - * @property {Record<string, string>} attributes - SVG attributes - * @property {Record<string, string>} styles - CSS styles - * @property {string} id - Element ID - * @property {string[]} classes - CSS classes - * @property {SVGElement[]} children - Child elements - */ export class SvgOptions { attributes; styles; |
