aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-14 11:47:49 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-14 11:47:49 +0200
commitd41ac3c094f733a8038885de3400ed7558b2b878 (patch)
treea9a7cd54900e0b0c66f3293f4ff6bc6ad5cbbec6 /app
parent6ca89c37f84c6b1d63c869e6471d3570d51f63be (diff)
downloadhousing-d41ac3c094f733a8038885de3400ed7558b2b878.tar.zst
Minor tuning
Diffstat (limited to 'app')
-rw-r--r--app/components.js36
-rw-r--r--app/dom.js6
-rw-r--r--app/main.js46
-rw-r--r--app/map.js9
-rw-r--r--app/models.js44
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));
}
diff --git a/app/dom.js b/app/dom.js
index 8e73a09..11d2a0a 100644
--- a/app/dom.js
+++ b/app/dom.js
@@ -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: {
diff --git a/app/map.js b/app/map.js
index d27a009..d63da17 100644
--- a/app/map.js
+++ b/app/map.js
@@ -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;
}