diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-14 21:39:29 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-15 14:03:04 +0200 |
| commit | 64acc82b9634d948517ec5bb2ebe5a33cdf22df6 (patch) | |
| tree | 0c82b618fa398caa2abcebeb573ac85ba29be3ef /app/map.js | |
| parent | 55085dae685305d24c29b60b1c16fc7dc76831af (diff) | |
| download | housing-64acc82b9634d948517ec5bb2ebe5a33cdf22df6.tar.zst | |
Cleanup
Diffstat (limited to '')
| -rw-r--r-- | app/map.js | 220 |
1 files changed, 127 insertions, 93 deletions
@@ -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"); + } }); } |
