diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-15 19:12:48 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-15 19:14:02 +0200 |
| commit | a44f2de806d0557b148dcdf36a3107cb4ecf31ce (patch) | |
| tree | 9868794be4dbf2be820add96dae560f95369fce9 | |
| parent | 64acc82b9634d948517ec5bb2ebe5a33cdf22df6 (diff) | |
| download | housing-a44f2de806d0557b148dcdf36a3107cb4ecf31ce.tar.zst | |
Minor fixes
| -rw-r--r-- | README.adoc | 2 | ||||
| -rw-r--r-- | app/components.js | 10 | ||||
| -rw-r--r-- | app/main.js | 4 | ||||
| -rw-r--r-- | app/map.js | 37 | ||||
| -rw-r--r-- | app/utm.js | 240 |
5 files changed, 260 insertions, 33 deletions
diff --git a/README.adoc b/README.adoc index b15752f..0101aec 100644 --- a/README.adoc +++ b/README.adoc @@ -64,6 +64,8 @@ go run main.go - Implement additional map features: "koulut", "päiväkodit" - Visual programming? Value function description with Javascript? +- When scoring weights are manipilated create a botton bar where are top x houses +- Make it possible to get left menu back - Notifications to user on new houses == Analysis Data processing diff --git a/app/components.js b/app/components.js index e1506a3..72c0595 100644 --- a/app/components.js +++ b/app/components.js @@ -789,7 +789,7 @@ export class Sidebar { children: [ Dom.heading( 3, - "Map Colors", + "Visualisation parameters", new DomOptions({ styles: { color: "#333", @@ -1303,7 +1303,7 @@ export class Modal { this.#dialog.append( Dom.button( "x", - () => this.hide(), + () => this.remove(), new DomOptions({ id: "close-modal-btn", styles: { @@ -1322,7 +1322,7 @@ export class Modal { ); // Add event listeners with AbortController - this.#dialog.addEventListener("close", () => this.hide(), { + this.#dialog.addEventListener("close", () => this.remove(), { signal: this.#abortController.signal, }); this.#dialog.addEventListener( @@ -1337,7 +1337,7 @@ export class Modal { "mouseleave", () => { if (!this.#persistent) { - this.#timer = window.setTimeout(() => this.hide(), 200); + this.#timer = window.setTimeout(() => this.remove(), 200); } }, { signal: this.#abortController.signal }, @@ -1568,7 +1568,7 @@ export class Modal { } } - hide() { + remove() { clearTimeout(this.#timer); this.#dialog.close(); this.#dialog.remove(); diff --git a/app/main.js b/app/main.js index 6e862c0..9a4da0c 100644 --- a/app/main.js +++ b/app/main.js @@ -332,10 +332,10 @@ export class App { */ #showHouseModal(houseId, persistent) { const house = this.#collection.houses.find((h) => h.id === houseId); - if (!house) return; + if (!house) throw new Error("Parameter is not a number!"); this.#map.setModalPersistence(persistent); - this.#modal?.hide(); + this.#modal?.remove(); this.#modal = new Modal({ house: house, @@ -79,8 +79,8 @@ export class MapEl { #modalTimer; /** @type {boolean} */ #persistentModal = false; - /** @type {Bounds|null} */ - #fullBounds = null; + /** @type {Bounds} */ + #fullBounds; /** @type {Point|null} */ #centerPoint = null; /** @type {number} */ @@ -150,29 +150,12 @@ export class MapEl { // Calculate initial center point and view height this.#centerPoint = new Point(bounds.minX + width / 2, bounds.minY + height / 2); - this.#viewHeightMeters = this.#calculateViewHeightMeters(height); - this.svg.setAttribute("viewBox", `${bounds.minX} ${-bounds.maxY} ${width} ${height}`); - } - - /** - * Calculate view height in meters based on latitude and view height in degrees - * @param {number} heightDegrees - * @returns {number} - */ - #calculateViewHeightMeters(heightDegrees) { // Approximate conversion: 1 degree latitude ≈ 111,000 meters // 1 degree longitude varies by latitude: 111,000 * cos(latitude) - return heightDegrees * 111000; - } + this.#viewHeightMeters = height * 111000; - /** - * Calculate view height in degrees based on meters and latitude - * @param {number} meters - * @returns {number} - */ - #calculateViewHeightDegrees(meters) { - return meters / 111000; + this.svg.setAttribute("viewBox", `${bounds.minX} ${-bounds.maxY} ${width} ${height}`); } /** @@ -191,7 +174,7 @@ export class MapEl { this.#viewHeightMeters = MapMath.clamp(this.#viewHeightMeters, 100, 1000000); // Calculate new view height in degrees - const newHeightDegrees = this.#calculateViewHeightDegrees(this.#viewHeightMeters); + const newHeightDegrees = this.#viewHeightMeters / 11000; // Calculate new width based on aspect ratio const aspectRatio = vb.width / vb.height; @@ -407,13 +390,15 @@ export class MapEl { * @param {SVGSVGElement} svg * @param {number} initVx - Initial velocity X * @param {number} initVy - Initial velocity Y + * @param {Bounds} fullBounds * @param {number} [friction=PanningConfig.DEFAULT_FRICTION] * @param {number} [speedThreshold=PanningConfig.DEFAULT_SPEED_THRESHOLD] */ - #startInertia( + static #startInertia( svg, initVx, initVy, + fullBounds, friction = PanningConfig.DEFAULT_FRICTION, speedThreshold = PanningConfig.DEFAULT_SPEED_THRESHOLD, ) { @@ -440,7 +425,7 @@ export class MapEl { vb.x -= deltaX; vb.y -= deltaY; - const { clampedX, clampedY } = MapEl.#clampViewBox(svg, this.#fullBounds); + const { clampedX, clampedY } = MapEl.#clampViewBox(svg, fullBounds); if (clampedX) currVx = -currVx * PanningConfig.DEFAULT_BOUNCE_FACTOR; if (clampedY) currVy = -currVy * PanningConfig.DEFAULT_BOUNCE_FACTOR; @@ -650,7 +635,7 @@ export class MapEl { const speed = Math.hypot(vx, vy); if (speed > PanningConfig.DEFAULT_SPEED_THRESHOLD) { - this.#startInertia(this.svg, vx, vy); + MapEl.#startInertia(this.svg, vx, vy, this.#fullBounds); } } }); @@ -720,7 +705,7 @@ export class MapEl { fill: "none", // Changed from semi-transparent blue to transparent "pointer-events": "stroke", stroke: "rgba(85, 85, 85, 1)", - "stroke-width": "0.001", + "stroke-width": "0.0005", }, }), ); diff --git a/app/utm.js b/app/utm.js new file mode 100644 index 0000000..1a8790f --- /dev/null +++ b/app/utm.js @@ -0,0 +1,240 @@ +/** + * UTM (Universal Transverse Mercator) coordinate conversion utilities + * Converts WGS84 coordinates (latitude/longitude) to UTM coordinates + */ +export class UTMConverter { + // WGS84 ellipsoid parameters + static WGS84_A = 6378137.0; // semi-major axis in meters + static WGS84_F = 1 / 298.257223563; // flattening + static WGS84_E = 0.081819191; // eccentricity + static WGS84_ESQ = 0.00669438; // eccentricity squared + + // UTM parameters + static UTM_SCALE_FACTOR = 0.9996; + static UTM_FALSE_EASTING = 500000; // in meters + static UTM_FALSE_NORTHING_N = 0; // for northern hemisphere + static UGM_FALSE_NORTHING_S = 10000000; // for southern hemisphere + + /** + * Convert WGS84 coordinates to UTM + * @param {number} longitude - Longitude in decimal degrees + * @param {number} latitude - Latitude in decimal degrees + * @returns {Object} UTM coordinates with zone information + */ + static toUTM(longitude, latitude) { + // Validate input coordinates + if (longitude < -180 || longitude > 180) { + throw new Error(`Longitude ${longitude} is out of valid range [-180, 180]`); + } + if (latitude < -90 || latitude > 90) { + throw new Error(`Latitude ${latitude} is out of valid range [-90, 90]`); + } + + const zone = UTMConverter.getUTMZone(longitude, latitude); + const hemisphere = latitude >= 0 ? "N" : "S"; + + // Convert to radians + const latRad = (latitude * Math.PI) / 180; + const lonRad = (longitude * Math.PI) / 180; + + // Central meridian for the zone (in radians) + const lonOrigin = (((zone - 1) * 6 - 180 + 3) * Math.PI) / 180; + const dLon = lonRad - lonOrigin; + + // Ellipsoid constants + const e = UTMConverter.WGS84_E; + const e2 = UTMConverter.WGS84_ESQ; + const e4 = e2 * e2; + const e6 = e4 * e2; + + const N = UTMConverter.WGS84_A / Math.sqrt(1 - e2 * Math.sin(latRad) * Math.sin(latRad)); + const T = Math.tan(latRad) * Math.tan(latRad); + const C = (e2 * Math.cos(latRad) * Math.cos(latRad)) / (1 - e2); + const A = Math.cos(latRad) * dLon; + + const M = + UTMConverter.WGS84_A * + ((1 - e2 / 4 - (3 * e4) / 64 - (5 * e6) / 256) * latRad - + ((3 * e2) / 8 + (3 * e4) / 32 + (45 * e6) / 1024) * Math.sin(2 * latRad) + + ((15 * e4) / 256 + (45 * e6) / 1024) * Math.sin(4 * latRad) - + ((35 * e6) / 3072) * Math.sin(6 * latRad)); + + // Calculate easting + const easting = + UTMConverter.UTM_SCALE_FACTOR * + N * + (A + + ((1 - T + C) * A * A * A) / 6 + + ((5 - 18 * T + T * T + 72 * C - 58 * e2) * A * A * A * A * A) / 120) + + UTMConverter.UTM_FALSE_EASTING; + + // Calculate northing + const northing = + UTMConverter.UTM_SCALE_FACTOR * + (M + + N * + Math.tan(latRad) * + ((A * A) / 2 + + ((5 - T + 9 * C + 4 * C * C) * A * A * A * A) / 24 + + ((61 - 58 * T + T * T + 600 * C - 330 * e2) * A * A * A * A * A * A) / 720)); + + // Adjust northing for southern hemisphere + const adjustedNorthing = + hemisphere === "S" ? northing + UTMConverter.UGM_FALSE_NORTHING_S : northing; + + return { + datum: "WGS84", + easting: Math.round(easting * 100) / 100, // Round to 2 decimal places + hemisphere: hemisphere, + northing: Math.round(adjustedNorthing * 100) / 100, + zone: zone, + }; + } + + /** + * Determine UTM zone from longitude and latitude + * @param {number} longitude - Longitude in decimal degrees + * @param {number} latitude - Latitude in decimal degrees + * @returns {number} UTM zone number + */ + static getUTMZone(longitude, latitude) { + // Special zones for Norway and Svalbard + if (latitude >= 56.0 && latitude < 64.0 && longitude >= 3.0 && longitude < 12.0) { + return 32; + } + + // Special zones for Svalbard + if (latitude >= 72.0 && latitude < 84.0) { + if (longitude >= 0.0 && longitude < 9.0) return 31; + if (longitude >= 9.0 && longitude < 21.0) return 33; + if (longitude >= 21.0 && longitude < 33.0) return 35; + if (longitude >= 33.0 && longitude < 42.0) return 37; + } + + // Standard zone calculation + let zone = Math.floor((longitude + 180) / 6) + 1; + + // Handle zones 31-37 for northern Europe special cases + if (latitude >= 72.0 && latitude < 84.0 && longitude >= 0.0 && longitude < 42.0) { + if (longitude < 9.0) zone = 31; + else if (longitude < 21.0) zone = 33; + else if (longitude < 33.0) zone = 35; + else zone = 37; + } + + return Math.min(Math.max(zone, 1), 60); + } + + /** + * Convert UTM coordinates back to WGS84 + * @param {number} easting - UTM easting in meters + * @param {number} northing - UTM northing in meters + * @param {number} zone - UTM zone number + * @param {string} hemisphere - 'N' for northern, 'S' for southern + * @returns {Object} WGS84 coordinates {longitude, latitude} + */ + static fromUTM(easting, northing, zone, hemisphere = "N") { + if (zone < 1 || zone > 60) { + throw new Error(`UTM zone ${zone} is out of valid range [1, 60]`); + } + + // Adjust northing for southern hemisphere + const adjustedNorthing = + hemisphere === "S" ? northing - UTMConverter.UGM_FALSE_NORTHING_S : northing; + + const e = UTMConverter.WGS84_E; + const e2 = UTMConverter.WGS84_ESQ; + const e4 = e2 * e2; + const e6 = e4 * e2; + + // Remove false easting and scale factor + const x = (easting - UTMConverter.UTM_FALSE_EASTING) / UTMConverter.UTM_SCALE_FACTOR; + const y = adjustedNorthing / UTMConverter.UTM_SCALE_FACTOR; + + // Footprint latitude + const M = y; + const mu = M / (UTMConverter.WGS84_A * (1 - e2 / 4 - (3 * e4) / 64 - (5 * e6) / 256)); + + const e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2)); + const fp = + mu + + ((3 * e1) / 2 - (27 * e1 * e1 * e1) / 32) * Math.sin(2 * mu) + + ((21 * e1 * e1) / 16 - (55 * e1 * e1 * e1 * e1) / 32) * Math.sin(4 * mu) + + ((151 * e1 * e1 * e1) / 96) * Math.sin(6 * mu); + + // Calculate latitude and longitude + const C1 = (e2 * Math.cos(fp) * Math.cos(fp)) / (1 - e2); + const T1 = Math.tan(fp) * Math.tan(fp); + const N1 = UTMConverter.WGS84_A / Math.sqrt(1 - e2 * Math.sin(fp) * Math.sin(fp)); + const R1 = (UTMConverter.WGS84_A * (1 - e2)) / (1 - e2 * Math.sin(fp) * Math.sin(fp)) ** 1.5; + const D = x / N1; + + const lat = + fp - + ((N1 * Math.tan(fp)) / R1) * + ((D * D) / 2 - + ((5 + 3 * T1 + 10 * C1 - 4 * C1 * C1 - 9 * e2) * D * D * D * D) / 24 + + ((61 + 90 * T1 + 298 * C1 + 45 * T1 * T1 - 252 * e2 - 3 * C1 * C1) * + D * + D * + D * + D * + D * + D) / + 720); + + const lonOrigin = (((zone - 1) * 6 - 180 + 3) * Math.PI) / 180; + const lon = + lonOrigin + + (D - + ((1 + 2 * T1 + C1) * D * D * D) / 6 + + ((5 - 2 * C1 + 28 * T1 - 3 * C1 * C1 + 8 * e2 + 24 * T1 * T1) * D * D * D * D * D) / 120) / + Math.cos(fp); + + return { + latitude: (lat * 180) / Math.PI, + longitude: (lon * 180) / Math.PI, + }; + } + + /** + * Convert Point geometry to UTM coordinates + * @param {Point} point - Point geometry + * @returns {Object} UTM coordinates + */ + static pointToUTM(point) { + return UTMConverter.toUTM(point.lng, point.lat); + } + + /** + * Convert multiple coordinates to UTM + * @param {Coordinate[]} coordinates - Array of [longitude, latitude] pairs + * @returns {Object[]} Array of UTM coordinates + */ + static coordinatesToUTM(coordinates) { + return coordinates.map((coord) => UTMConverter.toUTM(coord[0], coord[1])); + } +} + +// Convenience function for direct coordinate conversion +/** + * Convert WGS84 to UTM coordinates + * @param {number} lng - Longitude + * @param {number} lat - Latitude + * @returns {Object} UTM coordinates {easting, northing, zone, hemisphere} + */ +export function wgs84ToUTM(lng, lat) { + return UTMConverter.toUTM(lng, lat); +} + +/** + * Convert UTM to WGS84 coordinates + * @param {number} easting - UTM easting + * @param {number} northing - UTM northing + * @param {number} zone - UTM zone + * @param {string} hemisphere - 'N' or 'S' + * @returns {Object} WGS84 coordinates {longitude, latitude} + */ +export function utmToWGS84(easting, northing, zone, hemisphere = "N") { + return UTMConverter.fromUTM(easting, northing, zone, hemisphere); +} |
