diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-04 17:07:24 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-09 22:48:55 +0200 |
| commit | be7ec90b500ac68e053f2b58feb085247ef95817 (patch) | |
| tree | aef7732ce0bbe505c6bc8486e1d0da2c06990e6a /app/models.js | |
| parent | a4ed99a370930b1a0c0f065906ed99c15a015fd4 (diff) | |
| download | housing-be7ec90b500ac68e053f2b58feb085247ef95817.tar.zst | |
Refactor application to use couchbase
Diffstat (limited to 'app/models.js')
| -rw-r--r-- | app/models.js | 129 |
1 files changed, 80 insertions, 49 deletions
diff --git a/app/models.js b/app/models.js index d052f1c..d2c5b55 100644 --- a/app/models.js +++ b/app/models.js @@ -1,4 +1,4 @@ -import { Bounds, Collection, Feature, LineString, Point, Polygon } from "geom"; +import { Bounds, Collection, Feature, Geometry, LineString, Point, Polygon } from "geom"; /** @typedef {{ lat: number, lng: number }} GeoPointJson */ /** @typedef {{ date: string, price: number }} PriceUpdateJson */ @@ -160,10 +160,12 @@ export class Geospatial { export class District { /** * @param {string} name + * @param {string} municipality * @param {Polygon} polygon */ - constructor(name, polygon) { + constructor(name, municipality, polygon) { this.name = name; + this.municipality = municipality; this.polygon = polygon; } @@ -172,14 +174,20 @@ export class District { * @returns {District} */ static fromFeature(feature) { - const name = "nimi_fi" in feature.properties ? feature.properties.nimi_fi : ""; + const name = + "nimi_fi" in feature.properties && typeof feature.properties.nimi_fi === "string" + ? feature.properties.nimi_fi + : ""; + const municipality = + "kunta" in feature.properties && typeof feature.properties.kunta === "string" + ? feature.properties.kunta + : ""; const geometry = feature.geometry; - - if (name === undefined || !(geometry instanceof Polygon)) { - throw new Error("Invalid district feature data"); + if (!(geometry instanceof Polygon)) { + throw new Error(`Invalid district feature data ${geometry}`); } - return new District(name, geometry); + return new District(name, municipality, geometry); } /** @@ -235,11 +243,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 +275,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 +297,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 +309,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 +322,9 @@ export class House { address, district, coordinates, - postalCode = "", price, - buildingType, + postalCode = "", + buildingType = "", constructionYear = 0, rooms = 0, livingArea = 0, @@ -327,9 +333,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 +345,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 +396,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, @@ -489,51 +495,78 @@ export class Filters { } export class DataProvider { + static couchBaseUrl = "https://couch.tammi.cc"; + static wfsDbName = "helsinki_wfs"; + static housesDbName = "asunnot"; + + /** + * Fetch all features for a layer as a GeoJSON FeatureCollection + * @param {string} layerName + * @returns {Promise<Collection>} + */ + static async getCollectionFromCouch(layerName) { + // Use CouchDB view to get all features for the layer + const viewUrl = `${DataProvider.couchBaseUrl}/${DataProvider.wfsDbName}/_design/layers/_view/by_layer`; + const params = new URLSearchParams({ + // biome-ignore lint/style/useNamingConvention: header names are not optional + include_docs: "true", + key: JSON.stringify(layerName), + }); + + const response = await fetch(`${viewUrl}?${params}`, { + headers: new Headers({ accept: "application/json" }), + mode: "cors", + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const result = await response.json(); + const features = result.rows + .map((row) => { + return row.doc.geometry && "type" in row.doc.geometry + ? new Feature(Geometry.fromGeoJSON(row.doc.geometry), row.doc.properties, row.doc._id) + : null; + }) + .filter((x) => x !== null); + return new Collection(features); + } + /** @returns {Promise<Collection>} */ static async getCoastline() { - return await DataProvider.getCollection("data/rantaviiva.json"); + return await DataProvider.getCollectionFromCouch("Seutukartta_meren_rantaviiva"); } /** @returns {Promise<Collection>} */ static async getMainRoads() { - return await DataProvider.getCollection("data/paatiet.json"); + return await DataProvider.getCollectionFromCouch("Seutukartta_liikenne_paatiet"); } /** @returns {Promise<District[]>} */ static async getDistricts() { - const collection = await DataProvider.getCollection("data/districts.json"); - return District.fromCollection(collection); + const collection = await DataProvider.getCollectionFromCouch("Piirijako_peruspiiri"); + const districts = District.fromCollection(collection); + return districts; } /** @returns {Promise<TrainStation[]>} */ static async getTrainStations() { - const collection = await DataProvider.getCollection("data/juna_asemat.json"); + const collection = await DataProvider.getCollectionFromCouch("Seutukartta_liikenne_juna_asema"); return TrainStation.fromCollection(collection); } /** @returns {Promise<TrainTracks[]>} */ static async getTrainTracks() { - const collection = await DataProvider.getCollection("data/juna_radat.json"); + const collection = await DataProvider.getCollectionFromCouch("Seutukartta_liikenne_juna_rata"); return TrainTracks.fromCollection(collection); } - /** - * Load any GeoJSON file as Feature Collection - * @param {string} url - * @returns {Promise<Collection>} - */ - static async getCollection(url) { - const response = await fetch(url); - if (!response.ok) throw new Error(`Failed to load GeoJSON from ${url}: ${response.status}`); - const geojson = await response.json(); - return Collection.fromGeoJSON(geojson); - } - /** @returns {Promise<House[]>} */ static async getHouses() { try { const response = await fetch( - new URL("/asunnot/_all_docs?include_docs=true", "https://couch.tammi.cc"), + `${DataProvider.couchBaseUrl}/${DataProvider.housesDbName}/_all_docs?include_docs=true`, { headers: new Headers({ accept: "application/json" }), mode: "cors", @@ -546,8 +579,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`); |
