aboutsummaryrefslogtreecommitdiffstats
path: root/app/models.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/models.js')
-rw-r--r--app/models.js129
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`);