From 2113f8269423932fa76ae4f822f77a07dd703266 Mon Sep 17 00:00:00 2001 From: Petri Hienonen Date: Thu, 13 Nov 2025 13:23:25 +0200 Subject: Refactor, add light rail and tram stops --- app/models.js | 270 ++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 235 insertions(+), 35 deletions(-) (limited to 'app/models.js') 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 */ @@ -270,6 +270,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 */ @@ -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} + * @returns {Promise} */ - 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} */ + /** @returns {Promise} */ static async getCoastline() { - return await DataProvider.getCollectionFromCouch("Seutukartta_meren_rantaviiva"); + return await DataProvider.getFeaturesFromCouch("Seutukartta_meren_rantaviiva"); } - /** @returns {Promise} */ + /** @returns {Promise} */ static async getMainRoads() { - return await DataProvider.getCollectionFromCouch("Seutukartta_liikenne_paatiet"); + return await DataProvider.getFeaturesFromCouch("Seutukartta_liikenne_paatiet"); } /** @returns {Promise} */ 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} */ 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} */ 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} */ + static async getLightRailStops() { + return TrainStation.fromFeatureCollection( + await DataProvider.getFeaturesFromCouch("Seutukartta_liikenne_metroasemat"), + ); + } + + /** @returns {Promise} */ + static async getLightRailTracks() { + return TrainTracks.fromFeatureCollection( + await DataProvider.getFeaturesFromCouch("Seutukartta_liikenne_metro_rata"), + ); + } + + /** @returns {Promise} */ + static async getJokerTramStops() { + return await DataProvider.getFeaturesFromCouch("RaideJokeri_pysakit"); + } + + /** @returns {Promise} */ + static async getJokerTramTracks() { + return await DataProvider.getFeaturesFromCouch("RaideJokeri_ratalinja"); } /** @returns {Promise} */ 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} */ @@ -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); } } -- cgit v1.2.3-70-g09d2