aboutsummaryrefslogtreecommitdiffstats
path: root/app/main.js
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-04 17:07:24 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-09 22:48:55 +0200
commitbe7ec90b500ac68e053f2b58feb085247ef95817 (patch)
treeaef7732ce0bbe505c6bc8486e1d0da2c06990e6a /app/main.js
parenta4ed99a370930b1a0c0f065906ed99c15a015fd4 (diff)
downloadhousing-be7ec90b500ac68e053f2b58feb085247ef95817.tar.zst
Refactor application to use couchbase
Diffstat (limited to 'app/main.js')
-rw-r--r--app/main.js864
1 files changed, 347 insertions, 517 deletions
diff --git a/app/main.js b/app/main.js
index 617b9bb..1273a11 100644
--- a/app/main.js
+++ b/app/main.js
@@ -1,4 +1,5 @@
-import { Dom } from "dom";
+// main.js
+import { Dom, DomOptions, Modal, Widgets } from "dom";
import { ColorParameter, MapEl } from "map";
import {
DataProvider,
@@ -26,16 +27,14 @@ export class App {
#weights = new Weights();
/** @type {District[]} */
#districts = [];
- /** @type {MapEl|null} */
- #map = null;
+ /** @type {MapEl} */
+ #map;
/** @type {HTMLElement} */
#stats;
/** @type {HTMLElement} */
#controls;
- /** @type {HTMLDialogElement|null} */
+ /** @type {Modal|null} */
#modal = null;
- /** @type {number | null} */
- #modalTimer = null;
/** @type {boolean} */
#persistent = false;
/** @type {string} */
@@ -51,99 +50,105 @@ export class App {
margin: "0",
});
- const loading = App.createLoading();
-
- // Create main content container
- const mainContainer = Dom.div({
- styles: {
- display: "flex",
- flex: "1",
- overflow: "hidden",
- },
- });
-
- // Create map container
- const mapContainer = Dom.div({
- styles: {
- display: "flex",
- flex: "1",
- flexDirection: "column",
- minWidth: "0", // Prevents flex overflow
- },
- });
-
- const stats = Dom.div({
- styles: {
- background: "#fff",
- borderTop: "1px solid #ddd",
- flexShrink: "0",
- fontSize: "0.95rem",
- padding: "0.75rem 1rem",
- },
- });
-
- const controls = App.buildControls(
+ this.#controls = App.buildControls(
this.#filters,
this.#weights,
- () => this.#applyFilters(),
+ () => {
+ this.#filtered = this.#houses.filter((h) => h.matchesFilters(this.#filters));
+ if (this.#map) {
+ const filteredIds = this.#filtered.map((h) => h.id);
+ this.#map.updateHouseVisibility(filteredIds);
+ }
+ this.#updateStats();
+ },
(key, value) => {
if (key in this.#weights) {
this.#weights[/** @type {keyof Weights} */ (key)] = value;
}
- App.recalculateScores(this.#houses, this.#weights);
- this.#updateMapHouseColors();
+ App.#recalculateScores(this.#houses, this.#weights);
+ this.#map?.setColorParameter(this.#colorParameter);
this.#updateStats();
},
(param) => {
this.#colorParameter = param;
- this.#updateMapHouseColors();
+ this.#map?.setColorParameter(this.#colorParameter);
},
);
- // Build layout hierarchy
- mainContainer.append(controls, mapContainer);
- document.body.append(loading, mainContainer);
-
- this.#stats = stats;
- this.#controls = controls;
-
- // Initialize map
this.#map = new MapEl({
onHouseClick: (houseId, persistent) => this.#showHouseModal(houseId, persistent),
onHouseHover: (houseId, hide) => {
if (hide) {
- this.#hideModal();
+ this.#modal?.hide();
} else {
this.#showHouseModal(houseId, false);
}
},
});
- mapContainer.append(this.#map.initializeMap(), stats);
- this.#loadData(loading);
- }
- /**
- * Create loading indicator
- * @returns {HTMLElement}
- */
- static createLoading() {
- return Dom.div({
- styles: {
- background: "white",
- borderRadius: "8px",
- boxShadow: "0 2px 10px rgba(0,0,0,0.1)",
- color: "#555",
- fontSize: "1.2rem",
- left: "50%",
- padding: "2rem",
- position: "absolute",
- textAlign: "center",
- top: "50%",
- transform: "translate(-50%, -50%)",
- zIndex: "1000",
- },
- textContent: "Loading data…",
- });
+ this.#stats = Dom.div(
+ new DomOptions({
+ id: "stats",
+ styles: {
+ background: "#fff",
+ borderTop: "1px solid #ddd",
+ flexShrink: "0",
+ fontSize: "0.95rem",
+ padding: "0.75rem 1rem",
+ },
+ }),
+ );
+
+ const loading = Dom.span(
+ "Loading data…",
+ new DomOptions({
+ id: "loading",
+ styles: {
+ background: "white",
+ borderRadius: "8px",
+ boxShadow: "0 2px 10px rgba(0,0,0,0.1)",
+ color: "#555",
+ fontSize: "1.2rem",
+ left: "50%",
+ padding: "2rem",
+ position: "absolute",
+ textAlign: "center",
+ top: "50%",
+ transform: "translate(-50%, -50%)",
+ zIndex: "1000",
+ },
+ }),
+ );
+
+ document.body.append(
+ loading,
+ Dom.div(
+ new DomOptions({
+ children: [
+ this.#controls,
+ Dom.div(
+ new DomOptions({
+ children: [this.#map.svg, this.#stats],
+ id: "map-container",
+ styles: {
+ display: "flex",
+ flex: "1",
+ flexDirection: "column",
+ minWidth: "0", // Prevents flex overflow
+ },
+ }),
+ ),
+ ],
+ id: "main",
+ styles: {
+ display: "flex",
+ flex: "1",
+ overflow: "hidden",
+ },
+ }),
+ ),
+ );
+ this.#initialize(loading);
}
/**
@@ -156,255 +161,240 @@ export class App {
* @returns {HTMLElement}
*/
static buildControls(filters, weights, onFilterChange, onWeightChange, onColorChange) {
- const controls = Dom.div({
- styles: {
- background: "#fff",
- borderRight: "1px solid #ddd",
- display: "flex",
- flexDirection: "column",
- flexShrink: "0",
- gap: "1rem",
- overflowY: "auto",
- padding: "1rem",
- width: "300px",
- },
- });
-
- // Color parameter section
- const colorSection = Dom.div({
- styles: {
- borderBottom: "1px solid #eee",
- paddingBottom: "1rem",
- },
- });
-
- const colorTitle = Dom.heading(3, {
- styles: {
- color: "#333",
- fontSize: "1.1rem",
- margin: "0 0 1rem 0",
- },
- textContent: "Map Colors",
- });
-
- const colorGroup = Dom.div({
- styles: { display: "flex", flexDirection: "column" },
- });
-
- const colorLabel = Dom.label({
- for: "color-parameter",
- styles: { fontSize: "0.85rem", fontWeight: "bold", marginBottom: "0.25rem" },
- textContent: "Color houses by",
- });
-
- const colorSelect = Dom.select({
- id: "color-parameter",
- onChange: (e) => {
- const target = /** @type {HTMLSelectElement} */ (e.target);
- onColorChange(target.value);
- },
- styles: {
- border: "1px solid #ddd",
- borderRadius: "4px",
- fontSize: "0.9rem",
- padding: "0.5rem",
- },
- });
-
- colorSelect.append(
- Dom.option(ColorParameter.price, "Price"),
- Dom.option(ColorParameter.score, "Score"),
- Dom.option(ColorParameter.year, "Construction Year"),
- Dom.option(ColorParameter.area, "Living Area"),
+ const controls = Dom.div(
+ new DomOptions({
+ children: [
+ Dom.div(
+ new DomOptions({
+ children: [
+ Dom.heading(
+ 3,
+ "Map Colors",
+ new DomOptions({
+ styles: {
+ color: "#333",
+ fontSize: "1.1rem",
+ margin: "0 0 1rem 0",
+ },
+ }),
+ ),
+ Dom.div(
+ new DomOptions({
+ children: [
+ Dom.label(
+ "color-parameter",
+ "Color houses by",
+ new DomOptions({
+ styles: {
+ fontSize: "0.85rem",
+ fontWeight: "bold",
+ marginBottom: "0.25rem",
+ },
+ }),
+ ),
+ Dom.select(
+ (e) => {
+ const target = /** @type {HTMLSelectElement} */ (e.target);
+ onColorChange(target.value);
+ },
+ new DomOptions({
+ children: [
+ Dom.option(ColorParameter.price, "Price"),
+ Dom.option(ColorParameter.score, "Score"),
+ Dom.option(ColorParameter.year, "Construction Year"),
+ Dom.option(ColorParameter.area, "Living Area"),
+ ],
+ id: "color-parameter",
+ styles: {
+ border: "1px solid #ddd",
+ borderRadius: "4px",
+ fontSize: "0.9rem",
+ padding: "0.5rem",
+ },
+ }),
+ ),
+ ],
+ styles: { display: "flex", flexDirection: "column" },
+ }),
+ ),
+ ],
+ styles: {
+ borderBottom: "1px solid #eee",
+ paddingBottom: "1rem",
+ },
+ }),
+ ),
+ ],
+ id: "color-section",
+ styles: {
+ background: "#fff",
+ borderRight: "1px solid #ddd",
+ display: "flex",
+ flexDirection: "column",
+ flexShrink: "0",
+ gap: "1rem",
+ overflowY: "auto",
+ padding: "1rem",
+ width: "300px",
+ },
+ }),
);
- colorGroup.append(colorLabel, colorSelect);
- colorSection.append(colorTitle, colorGroup);
- controls.appendChild(colorSection);
-
- // Filter section
- const filterSection = Dom.div({
- styles: {
- borderBottom: "1px solid #eee",
- paddingBottom: "1rem",
- },
- });
-
- const filterTitle = Dom.heading(3, {
- styles: {
- color: "#333",
- fontSize: "1.1rem",
- margin: "0 0 1rem 0",
- },
- textContent: "Filters",
- });
-
- filterSection.appendChild(filterTitle);
-
- // Price filters in a row
- const priceRow = Dom.div({
- styles: {
- display: "flex",
- gap: "0.5rem",
- },
- });
-
- const minPriceFilter = App.addNumberFilter("min-price", "Min price (€)", (v) => {
- filters.minPrice = v ?? 0;
- onFilterChange();
- });
-
- const maxPriceFilter = App.addNumberFilter("max-price", "Max price (€)", (v) => {
- filters.maxPrice = v ?? Number.POSITIVE_INFINITY;
- onFilterChange();
- });
-
- priceRow.append(minPriceFilter, maxPriceFilter);
-
- const yearFilter = App.addNumberFilter("min-year", "Min year", (v) => {
- filters.minYear = v ?? 0;
- onFilterChange();
- });
-
- const areaFilter = App.addNumberFilter("min-area", "Min area (m²)", (v) => {
- filters.minArea = v ?? 0;
- onFilterChange();
- });
-
- // District multi-select
- const districtGroup = Dom.div({
- styles: { display: "flex", flexDirection: "column" },
- });
-
- const districtLabel = Dom.label({
- for: "district-select",
- styles: { fontSize: "0.85rem", fontWeight: "bold", marginBottom: "0.25rem" },
- textContent: "Districts",
- });
-
- const districtSelect = Dom.select({
- attributes: { multiple: "true" },
- id: "district-select",
- onChange: (e) => {
- const target = /** @type {HTMLSelectElement} */ (e.target);
- const selectedOptions = Array.from(target.selectedOptions).map((opt) => opt.value);
- filters.districts = selectedOptions;
- onFilterChange();
- },
- styles: {
- border: "1px solid #ddd",
- borderRadius: "4px",
- minHeight: "120px",
- padding: "0.5rem",
- },
- });
-
- districtGroup.append(districtLabel, districtSelect);
-
- filterSection.append(priceRow, yearFilter, areaFilter, districtGroup);
- controls.appendChild(filterSection);
-
- // Weights section
- const weightsSection = Dom.div({
- styles: {
- borderBottom: "1px solid #eee",
- paddingBottom: "1rem",
- },
- });
-
- const weightsTitle = Dom.heading(3, {
- styles: {
- color: "#333",
- fontSize: "1.1rem",
- margin: "0 0 1rem 0",
- },
- textContent: "Weights",
- });
-
- weightsSection.appendChild(weightsTitle);
-
- // Create weight sliders
- const weightSliders = [
- Dom.slider("w-price", "Price weight", "price", weights.price, onWeightChange),
- Dom.slider(
- "w-market",
- "Market distance",
- "distanceMarket",
- weights.distanceMarket,
- onWeightChange,
- ),
- Dom.slider(
- "w-school",
- "School distance",
- "distanceSchool",
- weights.distanceSchool,
- onWeightChange,
+ controls.append(
+ Dom.div(
+ new DomOptions({
+ children: [
+ Dom.heading(
+ 3,
+ "Filters",
+ new DomOptions({
+ styles: {
+ color: "#333",
+ fontSize: "1.1rem",
+ margin: "0 0 1rem 0",
+ },
+ }),
+ ),
+
+ Dom.div(
+ new DomOptions({
+ children: [
+ Widgets.numberFilter("min-price", "Min price (€)", (v) => {
+ filters.minPrice = v ?? 0;
+ onFilterChange();
+ }),
+
+ Widgets.numberFilter("max-price", "Max price (€)", (v) => {
+ filters.maxPrice = v ?? Number.POSITIVE_INFINITY;
+ onFilterChange();
+ }),
+ ],
+ id: "price-row",
+ styles: {
+ display: "flex",
+ gap: "0.5rem",
+ },
+ }),
+ ),
+ Widgets.numberFilter("min-year", "Min year", (v) => {
+ filters.minYear = v ?? 0;
+ onFilterChange();
+ }),
+
+ Widgets.numberFilter("min-area", "Min area (m²)", (v) => {
+ filters.minArea = v ?? 0;
+ onFilterChange();
+ }),
+ Dom.div(
+ new DomOptions({
+ children: [
+ Dom.label(
+ "district-select",
+ "Districts",
+ new DomOptions({
+ styles: { fontSize: "0.85rem", fontWeight: "bold", marginBottom: "0.25rem" },
+ }),
+ ),
+ Dom.select(
+ (e) => {
+ const target = /** @type {HTMLSelectElement} */ (e.target);
+ const selectedOptions = Array.from(target.selectedOptions).map(
+ (opt) => opt.value,
+ );
+ filters.districts = selectedOptions;
+ onFilterChange();
+ },
+ new DomOptions({
+ attributes: { multiple: "true" },
+ children: [],
+ id: "district-select",
+ styles: {
+ border: "1px solid #ddd",
+ borderRadius: "4px",
+ minHeight: "120px",
+ padding: "0.5rem",
+ },
+ }),
+ ),
+ ],
+ id: "district-multi-select",
+ styles: { display: "flex", flexDirection: "column" },
+ }),
+ ),
+ ],
+ id: "filter-section",
+ styles: {
+ borderBottom: "1px solid #eee",
+ paddingBottom: "1rem",
+ },
+ }),
),
- Dom.slider("w-crime", "Crime rate", "crimeRate", weights.crimeRate, onWeightChange),
- Dom.slider("w-safety", "Safety index", "safety", weights.safety, onWeightChange),
- Dom.slider("w-students", "S2 students", "s2Students", weights.s2Students, onWeightChange),
- Dom.slider(
- "w-railway",
- "Railway distance",
- "distanceRailway",
- weights.distanceRailway,
- onWeightChange,
+ Dom.div(
+ new DomOptions({
+ children: [
+ Dom.heading(
+ 3,
+ "Weights",
+ new DomOptions({
+ styles: {
+ color: "#333",
+ fontSize: "1.1rem",
+ margin: "0 0 1rem 0",
+ },
+ }),
+ ),
+ Widgets.slider("w-price", "Price weight", "price", weights.price, onWeightChange),
+ Widgets.slider(
+ "w-market",
+ "Market distance",
+ "distanceMarket",
+ weights.distanceMarket,
+ onWeightChange,
+ ),
+ Widgets.slider(
+ "w-school",
+ "School distance",
+ "distanceSchool",
+ weights.distanceSchool,
+ onWeightChange,
+ ),
+ Widgets.slider("w-crime", "Crime rate", "crimeRate", weights.crimeRate, onWeightChange),
+ Widgets.slider("w-safety", "Safety index", "safety", weights.safety, onWeightChange),
+ Widgets.slider(
+ "w-students",
+ "S2 students",
+ "s2Students",
+ weights.s2Students,
+ onWeightChange,
+ ),
+ Widgets.slider(
+ "w-railway",
+ "Railway distance",
+ "distanceRailway",
+ weights.distanceRailway,
+ onWeightChange,
+ ),
+ Widgets.slider(
+ "w-year",
+ "Construction year",
+ "constructionYear",
+ weights.constructionYear,
+ onWeightChange,
+ ),
+ ],
+ id: "weights-section",
+ styles: {
+ borderBottom: "1px solid #eee",
+ paddingBottom: "1rem",
+ },
+ }),
),
- Dom.slider(
- "w-year",
- "Construction year",
- "constructionYear",
- weights.constructionYear,
- onWeightChange,
- ),
- ];
-
- weightsSection.append(...weightSliders);
- controls.appendChild(weightsSection);
+ );
return controls;
}
/**
- * Create a number filter input
- * @param {string} id
- * @param {string} labelText
- * @param {(value: number | null) => void} onChange
- * @returns {HTMLElement}
- */
- static addNumberFilter(id, labelText, onChange) {
- const group = Dom.div({
- styles: { display: "flex", flexDirection: "column", marginBottom: "0.75rem" },
- });
-
- const label = Dom.label({
- for: id,
- styles: { fontSize: "0.85rem", fontWeight: "bold", marginBottom: "0.25rem" },
- textContent: labelText,
- });
-
- const input = Dom.input({
- id,
- onInput: /** @param {Event} e */ (e) => {
- const target = /** @type {HTMLInputElement} */ (e.target);
- const raw = target.value.trim();
- onChange(raw === "" ? null : Number(raw));
- },
- placeholder: "any",
- styles: {
- border: "1px solid #ddd",
- borderRadius: "4px",
- fontSize: "0.9rem",
- padding: "0.5rem",
- },
- type: "number",
- });
-
- group.append(label, input);
- return group;
- }
-
- /**
* Show modal with house details
* @param {string} houseId
* @param {boolean} persistent
@@ -418,172 +408,44 @@ export class App {
this.#map.setModalPersistence(persistent);
}
- // Remove existing modal
- this.#modal?.remove();
-
- // Create new modal
- this.#modal = Dom.buildModal(() => this.#hideModal());
- Object.assign(this.#modal.style, {
- left: "auto",
- maxHeight: "80vh",
- maxWidth: "400px",
- right: "20px",
- top: "50%",
- transform: "translateY(-50%)",
- width: "90%",
- });
-
- // Add hover grace period listeners
- this.#modal.addEventListener("mouseenter", () => {
- clearTimeout(this.#modalTimer);
- if (this.#map) {
- this.#map.clearModalTimer();
- }
- });
-
- this.#modal.addEventListener("mouseleave", () => {
- if (!this.#persistent) {
- this.#modalTimer = setTimeout(() => this.#hideModal(), 200);
- }
- });
-
- // Build modal content
- const content = this.#buildHouseModalContent(house);
- this.#modal.appendChild(content);
- document.body.appendChild(this.#modal);
-
- if (persistent) {
- this.#modal.showModal();
- } else {
- this.#modal.show();
- }
- }
-
- /**
- * Hide the modal
- */
- #hideModal() {
- this.#modal?.close();
- this.#modal?.remove();
- this.#modal = null;
- this.#persistent = false;
- clearTimeout(this.#modalTimer);
- if (this.#map) {
- this.#map.setModalPersistence(false);
- this.#map.clearModalTimer();
- }
- }
-
- /**
- * Build modal content for a house
- * @param {House} house
- * @returns {DocumentFragment}
- */
- #buildHouseModalContent(house) {
- const frag = document.createDocumentFragment();
-
- /* Header */
- const header = Dom.div({
- styles: {
- alignItems: "center",
- display: "flex",
- justifyContent: "space-between",
- marginBottom: "20px",
+ // Hide existing modal
+ this.#modal?.hide();
+
+ this.#modal = new Modal(
+ house,
+ persistent,
+ {
+ left: "auto",
+ maxHeight: "80vh",
+ maxWidth: "400px",
+ right: "20px",
+ top: "50%",
+ transform: "translateY(-50%)",
+ width: "90%",
},
- });
- const title = Dom.heading(2, {
- styles: { color: "#333", fontSize: "20px", margin: "0" },
- textContent: house.address,
- });
- const score = Dom.span({
- styles: {
- background: "#e8f5e9",
- borderRadius: "4px",
- color: "#2e7d32",
- fontSize: "16px",
- fontWeight: "bold",
- padding: "4px 8px",
+ () => {
+ this.#modal = null;
+ this.#persistent = false;
+ if (this.#map) {
+ this.#map.setModalPersistence(false);
+ this.#map.clearModalTimer();
+ }
},
- textContent: `Score: ${house.scores.current}`,
- });
- Dom.appendChildren(header, [title, score]);
- frag.appendChild(header);
-
- /* Details grid */
- const grid = Dom.div({
- styles: {
- display: "grid",
- gap: "15px",
- gridTemplateColumns: "repeat(2,1fr)",
- marginBottom: "20px",
+ () => {
+ if (this.#map) {
+ this.#map.clearModalTimer();
+ }
},
- });
- const details = [
- { label: "Price", value: `€${house.price.toLocaleString()}` },
- { label: "Building Type", value: house.buildingType },
- { label: "Construction Year", value: house.constructionYear?.toString() ?? "N/A" },
- { label: "Living Area", value: `${house.livingArea} m²` },
- { label: "District", value: house.district },
- { label: "Rooms", value: house.rooms?.toString() ?? "N/A" },
- ];
- for (const { label, value } of details) {
- const item = Dom.div({
- children: [
- Dom.div({
- styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "4px" },
- textContent: label,
- }),
- Dom.div({ styles: { color: "#333", fontSize: "14px" }, textContent: value }),
- ],
- });
- grid.appendChild(item);
- }
- frag.appendChild(grid);
-
- /* Description */
- const descSect = Dom.div({ styles: { marginBottom: "20px" } });
- const descTitle = Dom.div({
- styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "5px" },
- textContent: "Description",
- });
- const descText = Dom.p({
- styles: { color: "#333", fontSize: "14px", lineHeight: "1.4", marginTop: "5px" },
- textContent: house.description || "No description available.",
- });
- Dom.appendChildren(descSect, [descTitle, descText]);
- frag.appendChild(descSect);
-
- /* Images */
- if (house.images?.length) {
- const imgSect = Dom.div({ styles: { marginBottom: "20px" } });
- const imgTitle = Dom.div({
- styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "10px" },
- textContent: "Images",
- });
- const imgCont = Dom.div({
- styles: { display: "flex", gap: "10px", overflowX: "auto", paddingBottom: "5px" },
- });
- for (const src of house.images.slice(0, 3)) {
- imgCont.appendChild(
- Dom.img({
- attributes: { loading: "lazy" },
- src,
- styles: { borderRadius: "4px", flexShrink: "0", height: "100px" },
- }),
- );
- }
- Dom.appendChildren(imgSect, [imgTitle, imgCont]);
- frag.appendChild(imgSect);
- }
+ );
- return frag;
+ document.body.appendChild(this.#modal.render());
+ this.#modal.show();
}
-
/**
* Load data and initialize application
* @param {HTMLElement} loading
*/
- async #loadData(loading) {
+ async #initialize(loading) {
try {
const [districts, houses, trainStations, trainTracks, coastLine, mainRoads] =
await Promise.all([
@@ -594,7 +456,6 @@ export class App {
DataProvider.getCoastline(),
DataProvider.getMainRoads(),
]);
-
this.#districts = districts;
this.#houses = houses;
this.#trainStations = trainStations;
@@ -602,15 +463,18 @@ export class App {
this.#filtered = houses.slice();
- if (this.#map) {
- this.#map.setHouses(houses, this.#colorParameter);
- this.#map.setTrainData(trainStations, trainTracks);
- this.#map.setDistricts(districts);
- this.#map.setMapData(coastLine, mainRoads);
- }
+ this.#map.initialize(
+ districts,
+ coastLine,
+ mainRoads,
+ trainTracks,
+ trainStations,
+ houses,
+ this.#colorParameter,
+ );
// Populate district multi-select
- const districtOptions = App.renderDistrictOptions(this.#districts, this.#houses);
+ const districtOptions = App.#renderDistrictOptions(this.#districts, this.#houses);
const districtSelect = this.#controls.querySelector("#district-select");
if (districtSelect) {
districtSelect.append(...districtOptions);
@@ -623,62 +487,28 @@ export class App {
}
/**
- * Update house colors on map
- */
- #updateMapHouseColors() {
- if (this.#map) {
- this.#map.setColorParameter(this.#colorParameter);
- }
- }
-
- /**
* Render district options for multi-select
* @param {District[]} _districts
* @param {House[]} houses
* @returns {HTMLOptionElement[]}
*/
- static renderDistrictOptions(_districts, houses) {
+ static #renderDistrictOptions(_districts, houses) {
// Get unique districts from houses (they might have districts not in the district polygons)
const houseDistricts = [...new Set(houses.map((h) => h.district).filter((d) => d))].sort();
return houseDistricts.map((districtName) => Dom.option(districtName, districtName));
}
- #applyFilters() {
- this.#filtered = App.applyFilters(this.#houses, this.#filters);
-
- // Update map with filtered houses
- if (this.#map) {
- const filteredIds = this.#filtered.map((h) => h.id);
- this.#map.updateHouseVisibility(filteredIds);
- }
-
- this.#updateStats();
- }
-
- /**
- * Apply filters statically
- * @param {House[]} houses
- * @param {Filters} filters
- * @returns {House[]}
- */
- static applyFilters(houses, filters) {
- return houses.filter((h) => h.matchesFilters(filters));
- }
-
/**
* Recalculate scores statically
* @param {House[]} houses
* @param {Weights} weights
*/
- static recalculateScores(houses, weights) {
+ static #recalculateScores(houses, weights) {
for (const h of houses) {
h.scores.current = Math.round(ScoringEngine.calculate(h, weights));
}
}
- /**
- * Update stats display
- */
#updateStats() {
const count = this.#filtered.length;
const avg = count