aboutsummaryrefslogtreecommitdiffstats
path: root/app/map.js
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-14 21:39:29 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-15 14:03:04 +0200
commit64acc82b9634d948517ec5bb2ebe5a33cdf22df6 (patch)
tree0c82b618fa398caa2abcebeb573ac85ba29be3ef /app/map.js
parent55085dae685305d24c29b60b1c16fc7dc76831af (diff)
downloadhousing-64acc82b9634d948517ec5bb2ebe5a33cdf22df6.tar.zst
Cleanup
Diffstat (limited to 'app/map.js')
-rw-r--r--app/map.js220
1 files changed, 127 insertions, 93 deletions
diff --git a/app/map.js b/app/map.js
index d63da17..5976d92 100644
--- a/app/map.js
+++ b/app/map.js
@@ -1,5 +1,13 @@
import { Bounds, Feature, LineString, MultiLineString, Point } from "geom";
-import { AreaParam, Collection, District, House, HouseParameter, StatisticalArea } from "models";
+import {
+ AreaParam,
+ Collection,
+ District,
+ Filters,
+ House,
+ HouseParameter,
+ StatisticalArea,
+} from "models";
import { Svg, SvgOptions } from "svg";
/**
@@ -55,14 +63,18 @@ export class PanningConfig {
export class MapEl {
/** @type {SVGSVGElement} */
svg;
- /** @type {Collection|null} */
- #collection = null;
- /** @type {SVGGElement|null} */
- #housesGroup = null;
+ /** @type {Collection} */
+ #collection;
+ /** @type {SVGGElement} */
+ #housesGroup;
/** @type {Function} */
#onHouseClick;
/** @type {Function} */
#onHouseHover;
+ /** @type {HouseParameter} */
+ #houseParameter;
+ /** @type {AreaParam} */
+ #areaParameter;
/** @type {number|undefined} */
#modalTimer;
/** @type {boolean} */
@@ -79,8 +91,14 @@ export class MapEl {
* @param {Object} options
* @param {Function} options.onHouseClick
* @param {Function} options.onHouseHover
+ * @param {Collection} options.collection
+ * @param {HouseParameter} options.houseParameter
+ * @param {AreaParam} options.areaParameter
*/
constructor(options) {
+ this.#collection = options.collection;
+ this.#areaParameter = options.areaParameter;
+ this.#houseParameter = options.houseParameter;
const svg = Svg.svg(
new SvgOptions({
attributes: {
@@ -101,6 +119,24 @@ export class MapEl {
this.#onHouseClick = options.onHouseClick;
this.#onHouseHover = options.onHouseHover;
this.#enableControls(this.svg);
+
+ this.#setInitialViewBox(District.bounds(this.#collection.districts));
+ this.#fullBounds = Bounds.union([
+ Bounds.union(this.#collection.coastLine.features.map((f) => f.geometry.bounds())),
+ Bounds.union(this.#collection.mainRoads.features.map((f) => f.geometry.bounds())),
+ ]);
+
+ const layers = this.createMap(this.#collection, this.#areaParameter);
+ this.#housesGroup = MapEl.#createHouses({
+ houses: this.#collection.houses,
+ modalTimer: this.#modalTimer,
+ onHouseClick: this.#onHouseClick,
+ onHouseHover: this.#onHouseHover,
+ parameter: this.#houseParameter,
+ persistentModal: this.#persistentModal,
+ });
+ layers.appendChild(this.#housesGroup);
+ this.svg.append(layers);
}
/**
@@ -184,16 +220,85 @@ export class MapEl {
}
/**
- * Initialize map with empty content
+ * @param {object} o
+ * @param {House[]} o.houses
+ * @param {HouseParameter} o.parameter
+ * @param {number|undefined} o.modalTimer
+ * @param {Function} o.onHouseClick
+ * @param {Function} o.onHouseHover
+ * @param {boolean} o.persistentModal
+ * @returns {SVGGElement}
+ */
+ static #createHouses(o) {
+ const values = o.houses.map((house) => house.get(o.parameter)).sort();
+ const range = { max: Math.max(...values), min: Math.min(...values) };
+ switch (o.parameter) {
+ case HouseParameter.price: // No prices available for each house. Take some from the bottom
+ range.min = values[Math.floor(values.length * 0.2)];
+ range.max = values[Math.floor(values.length * 0.8)];
+ }
+ const housesEl = o.houses.map((house) => {
+ const normalized = MapMath.normalize(house.get(o.parameter), range.min, range.max);
+ const circle = Svg.circle(
+ house.coordinates,
+ new SvgOptions({
+ attributes: {
+ "data-id": house.id,
+ fill: Color.ocean(normalized),
+ },
+ children: [
+ Svg.title(`${house.address}, ${house.district}\n€${house.price.toLocaleString()}`),
+ ],
+ classes: ["house-marker"],
+ }),
+ );
+ circle.addEventListener("mouseenter", () => {
+ circle.setAttribute("r", "0.005");
+ clearTimeout(o.modalTimer);
+ o.onHouseHover(house.id, false);
+ });
+
+ circle.addEventListener("mouseleave", () => {
+ circle.setAttribute("r", "0.003");
+ circle.setAttribute("stroke", "rgba(51, 51, 51, 1)");
+ circle.setAttribute("stroke-width", "0.001");
+
+ if (!o.persistentModal && o.onHouseHover) {
+ o.modalTimer = window.setTimeout(() => {
+ o.onHouseHover(house.id, true);
+ }, 200);
+ }
+ });
+ circle.addEventListener("pointerdown", (e) => {
+ e.stopPropagation();
+ o.onHouseClick(house.id, true);
+ o.persistentModal = true;
+ });
+ return circle;
+ });
+
+ return Svg.g(
+ new SvgOptions({
+ attributes: {
+ "pointer-events": "visiblePainted",
+ r: "0.003",
+ stroke: "rgba(51, 51, 51, 1)",
+ "stroke-linecap": "butt",
+ "stroke-width": "0.001",
+ },
+ children: housesEl,
+ id: "houses",
+ }),
+ );
+ }
+
+ /**
* @param {Collection} c
- * @param {HouseParameter} houseParameter
* @param {AreaParam} areaParameter
- * @returns {SVGSVGElement}
+ * @returns {SVGGElement}
*/
- initialize(c, houseParameter, areaParameter) {
- this.#collection = c;
- this.#setInitialViewBox(District.bounds(c.districts));
- const transformGroup = Svg.g(
+ createMap(c, areaParameter) {
+ return Svg.g(
new SvgOptions({
attributes: { transform: "scale(1, -1)" },
children: [
@@ -291,29 +396,10 @@ export class MapEl {
id: "districts",
}),
),
- Svg.g(
- new SvgOptions({
- attributes: {
- "pointer-events": "visiblePainted",
- r: "0.003",
- stroke: "rgba(51, 51, 51, 1)",
- "stroke-linecap": "butt",
- "stroke-width": "0.001",
- },
- children: this.#getHouses(c.houses, houseParameter),
- id: "houses",
- }),
- ),
],
id: "map-transform",
}),
);
- this.svg.append(transformGroup);
- this.#fullBounds = Bounds.union([
- Bounds.union(c.coastLine.features.map((f) => f.geometry.bounds())),
- Bounds.union(c.mainRoads.features.map((f) => f.geometry.bounds())),
- ]);
- return this.svg;
}
/**
@@ -621,61 +707,6 @@ export class MapEl {
}
/**
- * Set houses data and render markers
- * @param {House[]} houses
- * @param {HouseParameter} param
- */
- #getHouses(houses, param) {
- const values = houses.map((house) => house.get(param)).sort();
- const range = { max: Math.max(...values), min: Math.min(...values) };
- switch (param) {
- case HouseParameter.price: // No prices available for each house. Take some from the bottom
- range.min = values[Math.floor(values.length * 0.2)];
- range.max = values[Math.floor(values.length * 0.8)];
- }
- return houses.map((house) => {
- const value = house.get(param);
- const normalized = MapMath.normalize(value, range.min, range.max);
- const circle = Svg.circle(
- house.coordinates,
- new SvgOptions({
- attributes: {
- "data-id": house.id,
- fill: Color.ocean(normalized),
- },
- children: [
- Svg.title(`${house.address}, ${house.district}\n€${house.price.toLocaleString()}`),
- ],
- classes: ["house-marker"],
- }),
- );
- circle.addEventListener("mouseenter", () => {
- circle.setAttribute("r", "0.005");
- clearTimeout(this.#modalTimer);
- this.#onHouseHover(house.id, false);
- });
-
- circle.addEventListener("mouseleave", () => {
- circle.setAttribute("r", "0.003");
- circle.setAttribute("stroke", "rgba(51, 51, 51, 1)");
- circle.setAttribute("stroke-width", "0.001");
-
- if (!this.#persistentModal && this.#onHouseHover) {
- this.#modalTimer = window.setTimeout(() => {
- this.#onHouseHover(house.id, true);
- }, 200);
- }
- });
- circle.addEventListener("click", (e) => {
- e.stopPropagation();
- this.#onHouseClick(house.id, true);
- this.#persistentModal = true;
- });
- return circle;
- });
- }
-
- /**
* Set districts data and render polygons
* @param {District[]} districts
*/
@@ -789,7 +820,7 @@ export class MapEl {
* Update house colors based on current color parameter
* @param {HouseParameter} param
*/
- updateHousesColor(param) {
+ updateHousesParameter(param) {
const values = this.#collection?.houses.map((house) => house.get(param)).sort();
if (!values) {
return;
@@ -845,15 +876,18 @@ export class MapEl {
/**
* Update house visibility based on filtered house IDs
- * @param {string[]} filteredHouseIds
+ * @param {Filters} filters
*/
- updateHouseVisibility(filteredHouseIds) {
- const filteredSet = new Set(filteredHouseIds);
+ updateHouseVisibility(filters) {
+ const ids = new Set(
+ this.#collection.houses.filter((h) => h.matchesFilters(filters)).map((h) => h.id),
+ );
const markers = this.#housesGroup?.querySelectorAll(".house-marker");
-
markers?.forEach((marker) => {
- const houseId = marker.id;
- marker.setAttribute("display", filteredSet.has(houseId) ? "" : "none");
+ const id = marker?.getAttribute("data-id");
+ if (id) {
+ marker.setAttribute("display", ids.has(id) ? "" : "none");
+ }
});
}