aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-07 10:35:28 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-07 10:35:28 +0200
commit08528a9e05a12e564f2c3776be4c8ae672f5054c (patch)
treeb5fe872701d0e30b5df18f7d4ea994b5b4a1761a
parentf3d3700dcca8555da9882f923a9fc4a30fcab3b8 (diff)
downloadhousing-08528a9e05a12e564f2c3776be4c8ae672f5054c.tar.zst
Minor dom cleanup
-rw-r--r--app/dom.js81
-rw-r--r--app/geometry.js16
-rw-r--r--app/main.js91
-rw-r--r--app/map.js6
-rw-r--r--app/models.js44
-rw-r--r--app/svg.js8
6 files changed, 85 insertions, 161 deletions
diff --git a/app/dom.js b/app/dom.js
index 5f94132..6e968bb 100644
--- a/app/dom.js
+++ b/app/dom.js
@@ -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);
}
/**
diff --git a/app/map.js b/app/map.js
index d3de4ac..b792704 100644
--- a/app/map.js
+++ b/app/map.js
@@ -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`);
diff --git a/app/svg.js b/app/svg.js
index 84d3aa6..ad63910 100644
--- a/app/svg.js
+++ b/app/svg.js
@@ -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;