diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-14 11:47:49 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-14 11:47:49 +0200 |
| commit | d41ac3c094f733a8038885de3400ed7558b2b878 (patch) | |
| tree | a9a7cd54900e0b0c66f3293f4ff6bc6ad5cbbec6 /app | |
| parent | 6ca89c37f84c6b1d63c869e6471d3570d51f63be (diff) | |
| download | housing-d41ac3c094f733a8038885de3400ed7558b2b878.tar.zst | |
Minor tuning
Diffstat (limited to '')
| -rw-r--r-- | app/components.js | 36 | ||||
| -rw-r--r-- | app/dom.js | 6 | ||||
| -rw-r--r-- | app/main.js | 46 | ||||
| -rw-r--r-- | app/map.js | 9 | ||||
| -rw-r--r-- | app/models.js | 44 |
5 files changed, 107 insertions, 34 deletions
diff --git a/app/components.js b/app/components.js index cc08fb4..c952b08 100644 --- a/app/components.js +++ b/app/components.js @@ -666,9 +666,9 @@ export class Modal { children: house.images.slice(0, 3).map((src) => { // Wrap image in anchor tag that opens in new tab return Dom.a( + src, new DomOptions({ attributes: { - href: src, rel: "noopener noreferrer", target: "_blank", }, @@ -771,6 +771,8 @@ export class Modal { { label: "Living Area", value: `${house.livingArea} m²` }, { label: "District", value: house.district }, { label: "Rooms", value: house.rooms?.toString() ?? "N/A" }, + { label: "Lot Size", value: house.totalArea ? `${house.totalArea} m²` : "N/A" }, + { label: "Price per m²", value: house.pricePerSqm ? `${house.pricePerSqm} €` : "N/A" }, ]; for (const { label, value } of details) { const item = Dom.div( @@ -816,6 +818,38 @@ export class Modal { ), ); + frag.appendChild( + Dom.div( + new DomOptions({ + children: [ + Dom.span( + "Official Listing", + new DomOptions({ + styles: { + fontSize: "14px", + fontWeight: "bold", + marginBottom: "5px", + marginRight: "10px", + }, + }), + ), + Dom.a( + house.url, + new DomOptions({ + attributes: { + rel: "noopener noreferrer", + target: "_blank", + }, + styles: { color: "#0066cc", fontSize: "14px", wordBreak: "break-all" }, + }), + "Oikotie", + ), + ], + styles: { marginBottom: "20px" }, + }), + ), + ); + if (house.images?.length) { frag.appendChild(Modal.imageSection(house)); } @@ -121,10 +121,14 @@ export class Dom { /** * Create a `<a>` + * @param {string} url * @param {DomOptions} o + * @param {string|undefined} text */ - static a(o) { + static a(url, o, text) { const link = document.createElement("a"); + if (text) link.text = text; + link.href = url; Object.assign(link.style, o.styles); if (o.id) link.id = o.id; for (const cls of o.classes) link.classList.add(cls); diff --git a/app/main.js b/app/main.js index 9abb300..da52152 100644 --- a/app/main.js +++ b/app/main.js @@ -50,23 +50,13 @@ export class App { this.#filters, this.#weights, () => { - this.#filtered = this.collection?.houses.filter((h) => h.matchesFilters(this.#filters)); - const filteredIds = this.#filtered.map((h) => h.id); - this.#map.updateHouseVisibility(filteredIds); - - const stats = App.#getStats(this.#filtered); - this.#stats.replaceWith(stats); - this.#stats = stats; + this.#applyFiltersAndScoring(); }, (key, value) => { if (key in this.#weights) { this.#weights[/** @type {keyof Weights} */ (key)] = value; } - App.#recalculateScores(this.collection?.houses, this.#weights); - this.#map.updateHousesColor(this.#houseParameter); - const stats = App.#getStats(this.#filtered); - this.#stats.replaceWith(stats); - this.#stats = stats; + this.#applyFiltersAndScoring(); }, (param) => { this.#houseParameter = param; @@ -189,7 +179,9 @@ export class App { async #initialize(loading) { try { this.collection = await Collection.get(); - this.#filtered = this.collection.houses.slice(); + + App.#recalculateScores(this.collection.houses, this.#weights); + this.#filtered = this.collection.houses.filter((h) => h.matchesFilters(this.#filters)); this.#map.initialize(this.collection, this.#houseParameter, this.#areaParameter); this.#sidebar.updateDistricts(this.collection.houses); @@ -211,10 +203,34 @@ export class App { static #recalculateScores(houses, weights) { for (const h of houses) { h.scores.current = Math.round(ScoringEngine.calculate(h, weights)); + h.value = h.scores.current; } } /** + * Apply filters and recalculate scores + */ + #applyFiltersAndScoring() { + if (!this.collection) return; + + // First recalculate all scores with current weights + App.#recalculateScores(this.collection.houses, this.#weights); + + // Then apply filters + this.#filtered = this.collection.houses.filter((h) => h.matchesFilters(this.#filters)); + + // Update map with filtered houses and new scores + const filteredIds = this.#filtered.map((h) => h.id); + this.#map.updateHouseVisibility(filteredIds); + this.#map.updateHousesColor(this.#houseParameter); + + // Update statistics + const stats = App.#getStats(this.#filtered); + this.#stats.replaceWith(stats); + this.#stats = stats; + } + + /** * Update statistics display using DOM methods * @param {House[]} filtered */ @@ -223,14 +239,14 @@ export class App { new DomOptions({ children: [ Dom.strong(filtered.length.toString()), - document.createTextNode(" houses shown • Average score: "), + Dom.span(" houses shown • Average score: "), Dom.strong( (filtered.length ? Math.round(filtered.reduce((s, h) => s + h.scores.current, 0) / filtered.length) : 0 ).toString(), ), - document.createTextNode(" • Use weights sliders to adjust scoring"), + Dom.span(" • Use weights sliders to adjust scoring"), ], id: "stats", styles: { @@ -817,14 +817,17 @@ export class MapEl { * @param {AreaParam} param */ updateArea(param) { - const values = this.#collection?.statisticalAreas.map((area) => area.getValue(param)); + const values = this.#collection?.statisticalAreas + .map((area) => area.getValue(param)) + .filter((x) => !Number.isNaN(x)) + .sort(); const range = { max: Math.max(...values), min: Math.min(...values) }; const statAreaPolygons = this.svg.querySelectorAll("#statistical-areas polygon"); statAreaPolygons.forEach((polygon) => { const areaId = polygon.getAttribute("data-id"); const area = this.#collection?.statisticalAreas.find((a) => a.id === areaId); - if (area) { - const value = area.getValue(param); + const value = area?.getValue(param); + if (area && value && !Number.isNaN(value)) { const normalized = MapMath.normalize(value, range.min, range.max); polygon.setAttribute( "fill", diff --git a/app/models.js b/app/models.js index 8cd1b5e..b4584f7 100644 --- a/app/models.js +++ b/app/models.js @@ -238,11 +238,17 @@ export class Geospatial { * @param {number} [schoolDistance] * @param {number} [railwayDistance] */ - constructor(marketDistance = 0, schoolDistance = 0, railwayDistance = 0) { + constructor( + marketDistance = Number.POSITIVE_INFINITY, + schoolDistance = Number.POSITIVE_INFINITY, + railwayDistance = Number.POSITIVE_INFINITY, + ) { this.marketDistance = marketDistance; this.schoolDistance = schoolDistance; this.railwayDistance = railwayDistance; - // Removed: crimeRate, safetyIndex, s2StudentRatio + this.distanceTrain = Number.POSITIVE_INFINITY; + this.distanceLightRail = Number.POSITIVE_INFINITY; + this.distanceTram = Number.POSITIVE_INFINITY; } /** @param {GeospatialJson} data @returns {Geospatial} */ @@ -538,6 +544,8 @@ export class House { * @param {Scores} scores * @param {string[]} images * @param {Geospatial} [geospatial] + * @param {StatisticalArea|null} statisticalArea + * @param {string} url */ constructor( id, @@ -563,6 +571,9 @@ export class House { images = [], geospatial = new Geospatial(), value = 0, + statisticalArea = null, + url = "", + pricePerSqm = 0, ) { this.id = id; this.address = address; @@ -587,10 +598,9 @@ export class House { this.images = images; this.geospatial = geospatial; this.value = value; - this.distanceTrain = 0; - this.distanceLightRail = 0; - this.distanceTram = 0; - this.statisticalArea = null; + this.statisticalArea = statisticalArea; + this.url = url; + this.pricePerSqm = pricePerSqm; } /** @param {HouseParameter} param */ @@ -659,6 +669,10 @@ export class House { new Scores(0), data.images || [], new Geospatial(), + 0, + null, + data.url || "", + rawData.pricePerSqm || 0, ); } @@ -795,11 +809,11 @@ export class Collection { house.statisticalArea = this.#findStatisticalArea(house.coordinates); // Calculate transit distances - house.distanceTrain = this.#calculateMinDistanceToStations( + house.geospatial.distanceTrain = this.#calculateMinDistanceToStations( house.coordinates, this.trainStations, ); - house.distanceLightRail = this.#calculateMinDistanceToStations( + house.geospatial.distanceLightRail = this.#calculateMinDistanceToStations( house.coordinates, this.lightRailStops, ); @@ -1065,20 +1079,22 @@ export class ScoringEngine { } // Transit distances (closer is better, but not too close) - if (weights.distanceTrain > 0 && house.distanceTrain > 0) { - const trainScore = ScoringEngine.calculateTransitScore(house.distanceTrain); + if (weights.distanceTrain > 0 && house.geospatial.distanceTrain > 0) { + const trainScore = ScoringEngine.calculateTransitScore(house.geospatial.distanceTrain); score += trainScore * weights.distanceTrain; totalWeight += weights.distanceTrain; } - if (weights.distanceLightRail > 0 && house.distanceLightRail > 0) { - const lightRailScore = ScoringEngine.calculateTransitScore(house.distanceLightRail); + if (weights.distanceLightRail > 0 && house.geospatial.distanceLightRail > 0) { + const lightRailScore = ScoringEngine.calculateTransitScore( + house.geospatial.distanceLightRail, + ); score += lightRailScore * weights.distanceLightRail; totalWeight += weights.distanceLightRail; } - if (weights.distanceTram > 0 && house.distanceTram > 0) { - const tramScore = ScoringEngine.calculateTransitScore(house.distanceTram); + if (weights.distanceTram > 0 && house.geospatial.distanceTram > 0) { + const tramScore = ScoringEngine.calculateTransitScore(house.geospatial.distanceTram); score += tramScore * weights.distanceTram; totalWeight += weights.distanceTram; } |
