aboutsummaryrefslogtreecommitdiffstats
path: root/download.js
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-09 22:59:02 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-11 15:35:03 +0200
commit909773f9d253c61183cc1f9f6193656957946be5 (patch)
tree136075e1946accedda0530dd25940b8931408c5a /download.js
parentbe7ec90b500ac68e053f2b58feb085247ef95817 (diff)
downloadhousing-909773f9d253c61183cc1f9f6193656957946be5.tar.zst
Add statistical areas
Diffstat (limited to '')
-rw-r--r--download.js81
1 files changed, 68 insertions, 13 deletions
diff --git a/download.js b/download.js
index 90a7d81..6a1e67e 100644
--- a/download.js
+++ b/download.js
@@ -1,10 +1,15 @@
-import crypto from "crypto";
-import fs from "fs";
-import path from "path";
+import crypto from "node:crypto";
+import fs from "node:fs";
+import path from "node:path";
const couchUsername = process.env.COUCHDB_USERNAME;
const couchPassword = process.env.COUCHDB_PASSWORD;
+/**
+ * Generates the Basic Auth header for CouchDB using environment variables.
+ * @returns {string} The Basic Auth header string.
+ * @throws {Error} If CouchDB credentials are not set.
+ */
function getAuthHeader() {
if (!couchUsername || !couchPassword) {
throw new Error("CouchDB credentials not set in environment variables");
@@ -13,7 +18,6 @@ function getAuthHeader() {
return `Basic ${auth}`;
}
-// === CONFIG ===
const baseUrl = "https://kartta.hel.fi/ws/geoserver/avoindata/wfs";
const layers = [
"Aluesarjat_avainluvut_2024",
@@ -41,6 +45,10 @@ if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
+/**
+ * Creates headers for CouchDB requests, including authorization.
+ * @returns {Headers} The Headers object for fetch requests.
+ */
function getHeaders() {
return new Headers({
// biome-ignore lint/style/useNamingConvention: database
@@ -49,7 +57,11 @@ function getHeaders() {
});
}
-// === COUCHDB HELPERS ===
+/**
+ * Creates the CouchDB database if it doesn't exist.
+ * @returns {Promise<void>}
+ * @throws {Error} If database creation fails (other than already exists).
+ */
async function createDatabase() {
const url = `${couchUrl}/${dbName}`;
const res = await fetch(url, {
@@ -58,11 +70,16 @@ async function createDatabase() {
});
if (res.ok || res.status === 412) {
console.log(`Database ${dbName} ready.`);
+ return;
} else {
throw new Error(await res.text());
}
}
+/**
+ * Ensures the design documents (views) exist in the database, creating or updating as needed.
+ * @returns {Promise<void>}
+ */
async function ensureDesignDocs() {
const designDoc = {
_id: "_design/layers",
@@ -87,6 +104,7 @@ async function ensureDesignDocs() {
method: "PUT",
});
console.log("Created design document: layers/by_layer");
+ return;
} else if (res.ok) {
const existing = await res.json();
designDoc._rev = existing._rev;
@@ -96,10 +114,18 @@ async function ensureDesignDocs() {
method: "PUT",
});
console.log("Updated design document");
+ return;
}
+ // If neither, implicitly return void, but log unexpected status
+ console.warn(`Unexpected status when ensuring design docs: ${res.status}`);
}
-// === DOWNLOAD ===
+/**
+ * Downloads a GeoJSON layer from the WFS service.
+ * @param {string} layer - The name of the layer to download.
+ * @returns {Promise<object>} The parsed GeoJSON object.
+ * @throws {Error} If the fetch fails.
+ */
async function downloadLayer(layer) {
const url = `${baseUrl}?service=WFS&version=2.0.0&request=GetFeature&typeName=avoindata:${layer}&outputFormat=json&srsname=EPSG:4326`;
const res = await fetch(url);
@@ -108,13 +134,26 @@ async function downloadLayer(layer) {
return response;
}
+/**
+ * Saves GeoJSON data to a local file.
+ * Note: This function is defined but not currently used in the script. It could be called in processLayer if local saving is desired.
+ * @param {string} layer - The layer name for the file.
+ * @param {object} data - The GeoJSON data to save.
+ * @returns {void}
+ */
function saveToFile(layer, data) {
const filePath = path.join(outputDir, `${layer}.geojson`);
fs.writeFileSync(filePath, JSON.stringify(data, null, "\t"));
console.log(`Saved: ${layer}.geojson`);
}
-// === UPLOAD METADATA ===
+/**
+ * Uploads or updates metadata for a layer in CouchDB.
+ * @param {string} layer - The layer name.
+ * @param {number} featureCount - The number of features in the layer.
+ * @returns {Promise<void>}
+ * @throws {Error} If the upload fails.
+ */
async function uploadLayerMetadata(layer, featureCount) {
const docId = `layer_metadata:${layer}`;
@@ -142,9 +181,15 @@ async function uploadLayerMetadata(layer, featureCount) {
});
if (!putRes.ok) throw new Error(await putRes.text());
console.log(`Metadata updated: ${layer} (${featureCount} features)`);
+ return;
}
-// === UPLOAD SINGLE FEATURE (with deduplication) ===
+/**
+ * Uploads a single feature document to CouchDB, with deduplication check.
+ * @param {object} doc - The feature document to upload.
+ * @returns {Promise<boolean>} True if uploaded/updated, false if skipped (no changes).
+ * @throws {Error} If the upload fails.
+ */
async function uploadFeature(doc) {
const url = `${couchUrl}/${dbName}/${doc._id}`;
const getRes = await fetch(url, { headers: getHeaders() });
@@ -165,15 +210,20 @@ async function uploadFeature(doc) {
method: "PUT",
});
- return putRes.ok;
+ if (!putRes.ok) throw new Error(await putRes.text());
+ return true; // uploaded or updated
}
-// === PROCESS LAYER ===
+/**
+ * Processes a single layer: downloads GeoJSON, uploads features with dedup, and updates metadata.
+ * @param {string} layer - The layer to process.
+ * @returns {Promise<{uploaded: number, skipped: number}>} Counts of uploaded and skipped features.
+ * @throws {Error} If download or uploads fail.
+ */
async function processLayer(layer) {
const geojson = await downloadLayer(layer);
if (!geojson || !geojson.features) {
- console.warn(`No features in ${layer} ${geojson}`);
- process.exit(1);
+ throw new Error(`No features in ${layer}: ${JSON.stringify(geojson)}`);
}
let uploaded = 0;
@@ -204,9 +254,13 @@ async function processLayer(layer) {
await uploadLayerMetadata(layer, geojson.features.length);
console.log(`Done: ${layer} | Uploaded: ${uploaded} | Skipped: ${skipped}`);
+ return { skipped, uploaded };
}
-// === MAIN ===
+/**
+ * Main entry point: sets up database, processes all layers.
+ * @returns {Promise<void>}
+ */
async function main() {
await createDatabase();
await ensureDesignDocs();
@@ -218,6 +272,7 @@ async function main() {
}
console.log("All layers processed.");
+ return;
}
if (process.argv[1] === new URL(import.meta.url).pathname) {