aboutsummaryrefslogtreecommitdiffstats
path: root/app/models.js
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-13 13:23:25 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-13 16:01:24 +0200
commit2113f8269423932fa76ae4f822f77a07dd703266 (patch)
tree564a92220ab89b91c17efa63fc94549a90445573 /app/models.js
parent4372d56a51a9596b1636d133b03057b0ba84c920 (diff)
downloadhousing-2113f8269423932fa76ae4f822f77a07dd703266.tar.zst
Refactor, add light rail and tram stops
Diffstat (limited to 'app/models.js')
-rw-r--r--app/models.js270
1 files changed, 235 insertions, 35 deletions
diff --git a/app/models.js b/app/models.js
index b100ac8..9ab9c84 100644
--- a/app/models.js
+++ b/app/models.js
@@ -1,4 +1,4 @@
-import { Bounds, Collection, Feature, Geometry, LineString, Point, Polygon } from "geom";
+import { Bounds, Feature, FeatureCollection, Geometry, LineString, Point, Polygon } from "geom";
/** @typedef {{ lat: number, lng: number }} GeoPointJson */
/** @typedef {{ date: string, price: number }} PriceUpdateJson */
@@ -271,6 +271,18 @@ export class Geospatial {
}
/**
+ * Area color parameters for statistical areas
+ * @enum {string}
+ */
+export const AreaParam = {
+ averageIncome: "averageIncome",
+ foreignSpeakers: "foreignSpeakers",
+ higherEducation: "higherEducation",
+ none: "none",
+ unemploymentRate: "unemploymentRate",
+};
+
+/**
* Represents a statistical area with demographic and housing data
*/
export class StatisticalArea {
@@ -283,6 +295,50 @@ export class StatisticalArea {
this.id = id;
this.polygon = polygon;
this.properties = properties;
+ this.averageIncome = properties.tu_kesk;
+ this.foreignSpeakers = properties.vr_kiel_vier / properties.vr_vakiy;
+ this.higherEducation = properties.ko_yl_kork / properties.ko_25_;
+ this.unemploymentRate = properties.tp_tyotaste;
+ }
+
+ /**
+ * Get display text for statistical area tooltip
+ * @param {AreaParam} param
+ * @returns {string}
+ */
+ getDisplay(param) {
+ switch (param) {
+ case AreaParam.foreignSpeakers:
+ return `Foreign speakers: ${(this.foreignSpeakers * 100).toFixed(1)}%`;
+ case AreaParam.unemploymentRate:
+ return `Unemployment rate: ${this.unemploymentRate?.toFixed(1)}%`;
+ case AreaParam.averageIncome:
+ return `Average income: ${Math.round(this.averageIncome).toLocaleString()} €`;
+ case AreaParam.higherEducation:
+ return `Higher education: ${(this.higherEducation * 100).toFixed(1)}%`;
+ default:
+ return "";
+ }
+ }
+
+ /**
+ * Get value for statistical area based on parameter
+ * @param {string} param
+ * @returns {number}
+ */
+ getValue(param) {
+ switch (param) {
+ case AreaParam.foreignSpeakers:
+ return this.foreignSpeakers;
+ case AreaParam.unemploymentRate:
+ return this.unemploymentRate;
+ case AreaParam.averageIncome:
+ return this.averageIncome;
+ case AreaParam.higherEducation:
+ return this.higherEducation;
+ default:
+ return 0;
+ }
}
/**
@@ -299,11 +355,10 @@ export class StatisticalArea {
}
/**
- * Convert Collection to StatisticalArea[]
- * @param {Collection} collection
+ * @param {FeatureCollection} collection
* @returns {StatisticalArea[]}
*/
- static fromCollection(collection) {
+ static fromFeatureCollection(collection) {
return collection.features.map(StatisticalArea.fromFeature);
}
@@ -380,11 +435,11 @@ export class District {
}
/**
- * Convert Collection to District[]
- * @param {Collection} collection
+ * Convert FeatureCollection to District[]
+ * @param {FeatureCollection} collection
* @returns {District[]}
*/
- static fromCollection(collection) {
+ static fromFeatureCollection(collection) {
return collection.features.map(District.fromFeature);
}
@@ -421,11 +476,11 @@ export class TrainTracks {
}
/**
- * Convert Collection to TrainTracks[]
- * @param {Collection} collection
+ * Convert FeatureCollection to TrainTracks[]
+ * @param {FeatureCollection} collection
* @returns {TrainTracks[]}
*/
- static fromCollection(collection) {
+ static fromFeatureCollection(collection) {
return collection.features.map(TrainTracks.fromFeature);
}
}
@@ -454,15 +509,26 @@ export class TrainStation {
}
/**
- * Convert Collection to TrainStation[]
- * @param {Collection} collection
+ * Convert FeatureCollection to TrainStation[]
+ * @param {FeatureCollection} collection
* @returns {TrainStation[]}
*/
- static fromCollection(collection) {
+ static fromFeatureCollection(collection) {
return collection.features.map(TrainStation.fromFeature).filter((x) => x !== null);
}
}
+/**
+ * Color parameters for house markers
+ * @enum {string}
+ */
+export const HouseParameter = {
+ area: "livingArea",
+ price: "price",
+ score: "score",
+ year: "constructionYear",
+};
+
export class House {
/**
* @param {string} id
@@ -511,6 +577,7 @@ export class House {
scores = new Scores(0),
images = [],
geospatial = new Geospatial(),
+ value = 0,
) {
this.id = id;
this.address = address;
@@ -534,9 +601,27 @@ export class House {
this.scores = scores;
this.images = images;
this.geospatial = geospatial;
+ this.value = value;
}
- /** @param {HouseJson} data @returns {House} */
+ /** @param {HouseParameter} param */
+ get(param) {
+ switch (param) {
+ case HouseParameter.price:
+ return this.price;
+ case HouseParameter.score:
+ return this.value;
+ case HouseParameter.year:
+ return this.constructionYear;
+ case HouseParameter.area:
+ return this.livingArea;
+ default:
+ return 0;
+ }
+ }
+
+ /** @param {HouseJson} data
+ @returns {House} */
static fromJson(data) {
// Parse price from string like "260 000 €" to number
/** @param {string} priceStr @returns {number} */
@@ -658,13 +743,102 @@ export class Filters {
constructor() {
this.minPrice = 0;
this.maxPrice = Number.POSITIVE_INFINITY;
- this.minYear = 0;
+ this.minYear = 1800;
this.minArea = 0;
/** @type {string[]} */
this.districts = [];
}
}
+export class Collection {
+ /**
+ * @param {District[]} districts
+ * @param {House[]} houses
+ * @param {TrainStation[]} trainStations *
+ * @param {TrainTracks[]} trainTracks
+ * @param {FeatureCollection} coastLine
+ * @param {FeatureCollection} mainRoads
+ * @param {StatisticalArea[]} statisticalAreas
+ * @param {FeatureCollection} jokerTramStops,
+ * @param {FeatureCollection} jokerTramTracks,
+ * @param {TrainStation[]} lightRailStops,
+ * @param {TrainTracks[]} lightRailTracks,
+ */
+ constructor(
+ districts,
+ houses,
+ trainStations,
+ trainTracks,
+ coastLine,
+ mainRoads,
+ statisticalAreas,
+ jokerTramStops,
+ //jokerTramTracks,
+ lightRailStops,
+ lightRailTracks,
+ ) {
+ this.districts = districts;
+ this.houses = houses;
+ this.trainStations = trainStations;
+ this.trainTracks = trainTracks;
+ this.coastLine = coastLine;
+ this.mainRoads = mainRoads;
+ this.statisticalAreas = statisticalAreas;
+ this.jokerTramStops = jokerTramStops;
+ //this.jokerTramTracks = jokerTramTracks;
+ this.lightRailStops = lightRailStops;
+ this.lightRailTracks = lightRailTracks;
+ }
+
+ static async get() {
+ const [
+ districts,
+ houses,
+ trainStations,
+ trainTracks,
+ coastLine,
+ mainRoads,
+ statisticalAreas,
+ jokerTramStops,
+ //jokerTramTracks,
+ lightRailStops,
+ lightRailTracks,
+ ] = await Promise.all([
+ await DataProvider.getDistricts(),
+ await DataProvider.getHouses(),
+ await DataProvider.getTrainStations(),
+ await DataProvider.getTrainTracks(),
+ await DataProvider.getCoastline(),
+ await DataProvider.getMainRoads(),
+ await DataProvider.getStatisticalAreas(),
+ await DataProvider.getJokerTramStops(),
+ //await DataProvider.getJokerTramTracks(),
+ await DataProvider.getLightRailStops(),
+ await DataProvider.getLightRailTracks(),
+ ]);
+ return new Collection(
+ districts,
+ houses,
+ trainStations,
+ trainTracks,
+ coastLine,
+ mainRoads,
+ statisticalAreas,
+ jokerTramStops,
+ //jokerTramTracks,
+ lightRailStops,
+ lightRailTracks,
+ );
+ }
+
+ /**
+ * @param {Point} point
+ */
+ static getDistanceToRail(point) {
+ return 0;
+ }
+}
+
export class DataProvider {
static couchBaseUrl = "https://couch.tammi.cc";
static wfsDbName = "helsinki_wfs";
@@ -673,9 +847,9 @@ export class DataProvider {
/**
* Fetch all features for a layer as a GeoJSON FeatureCollection
* @param {string} layerName
- * @returns {Promise<Collection>}
+ * @returns {Promise<FeatureCollection>}
*/
- static async getCollectionFromCouch(layerName) {
+ static async getFeaturesFromCouch(layerName) {
// Use CouchDB view to get all features for the layer
const viewUrl = new URL(
`/${DataProvider.wfsDbName}/_design/layers/_view/by_layer`,
@@ -705,42 +879,69 @@ export class DataProvider {
: null;
})
.filter((x) => x !== null);
- return new Collection(features);
+ return new FeatureCollection(features);
}
- /** @returns {Promise<Collection>} */
+ /** @returns {Promise<FeatureCollection>} */
static async getCoastline() {
- return await DataProvider.getCollectionFromCouch("Seutukartta_meren_rantaviiva");
+ return await DataProvider.getFeaturesFromCouch("Seutukartta_meren_rantaviiva");
}
- /** @returns {Promise<Collection>} */
+ /** @returns {Promise<FeatureCollection>} */
static async getMainRoads() {
- return await DataProvider.getCollectionFromCouch("Seutukartta_liikenne_paatiet");
+ return await DataProvider.getFeaturesFromCouch("Seutukartta_liikenne_paatiet");
}
/** @returns {Promise<District[]>} */
static async getDistricts() {
- const collection = await DataProvider.getCollectionFromCouch("Piirijako_peruspiiri");
- const districts = District.fromCollection(collection);
- return districts;
+ return District.fromFeatureCollection(
+ await DataProvider.getFeaturesFromCouch("Piirijako_peruspiiri"),
+ );
}
/** @returns {Promise<TrainStation[]>} */
static async getTrainStations() {
- const collection = await DataProvider.getCollectionFromCouch("Seutukartta_liikenne_juna_asema");
- return TrainStation.fromCollection(collection);
+ return TrainStation.fromFeatureCollection(
+ await DataProvider.getFeaturesFromCouch("Seutukartta_liikenne_juna_asema"),
+ );
}
/** @returns {Promise<TrainTracks[]>} */
static async getTrainTracks() {
- const collection = await DataProvider.getCollectionFromCouch("Seutukartta_liikenne_juna_rata");
- return TrainTracks.fromCollection(collection);
+ return TrainTracks.fromFeatureCollection(
+ await DataProvider.getFeaturesFromCouch("Seutukartta_liikenne_juna_rata"),
+ );
+ }
+
+ /** @returns {Promise<TrainStation[]>} */
+ static async getLightRailStops() {
+ return TrainStation.fromFeatureCollection(
+ await DataProvider.getFeaturesFromCouch("Seutukartta_liikenne_metroasemat"),
+ );
+ }
+
+ /** @returns {Promise<TrainTracks[]>} */
+ static async getLightRailTracks() {
+ return TrainTracks.fromFeatureCollection(
+ await DataProvider.getFeaturesFromCouch("Seutukartta_liikenne_metro_rata"),
+ );
+ }
+
+ /** @returns {Promise<FeatureCollection>} */
+ static async getJokerTramStops() {
+ return await DataProvider.getFeaturesFromCouch("RaideJokeri_pysakit");
+ }
+
+ /** @returns {Promise<FeatureCollection>} */
+ static async getJokerTramTracks() {
+ return await DataProvider.getFeaturesFromCouch("RaideJokeri_ratalinja");
}
/** @returns {Promise<StatisticalArea[]>} */
static async getStatisticalAreas() {
- const collection = await DataProvider.getCollectionFromCouch("Aluesarjat_avainluvut_2024");
- return StatisticalArea.fromCollection(collection);
+ return StatisticalArea.fromFeatureCollection(
+ await DataProvider.getFeaturesFromCouch("Aluesarjat_avainluvut_2024"),
+ );
}
/** @returns {Promise<House[]>} */
@@ -762,7 +963,7 @@ export class DataProvider {
return [];
}
- const data = /** @type {ApiResponse} */ (await response.json());
+ const data = await response.json();
const housesData = data.rows.map((row) => row.doc).filter((doc) => doc?.raw?.location);
console.log(`Loaded ${housesData.length} houses from API`);
@@ -774,13 +975,12 @@ export class DataProvider {
}
/**
- * Convert houses to Feature Collection for export
* @param {House[]} houses
- * @returns {Collection}
+ * @returns {FeatureCollection}
*/
static housesToCollection(houses) {
const features = houses.map((house) => house.toFeature());
- return new Collection(features);
+ return new FeatureCollection(features);
}
}