aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-11-15 19:12:48 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-11-15 19:14:02 +0200
commita44f2de806d0557b148dcdf36a3107cb4ecf31ce (patch)
tree9868794be4dbf2be820add96dae560f95369fce9
parent64acc82b9634d948517ec5bb2ebe5a33cdf22df6 (diff)
downloadhousing-a44f2de806d0557b148dcdf36a3107cb4ecf31ce.tar.zst
Minor fixes
-rw-r--r--README.adoc2
-rw-r--r--app/components.js10
-rw-r--r--app/main.js4
-rw-r--r--app/map.js37
-rw-r--r--app/utm.js240
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,
diff --git a/app/map.js b/app/map.js
index 5976d92..e8e9098 100644
--- a/app/map.js
+++ b/app/map.js
@@ -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);
+}