From 73b3407f040cd755122ee0db37857b121eb1b7f2 Mon Sep 17 00:00:00 2001 From: Matteo Rosati Date: Sun, 25 Jan 2026 09:27:50 +0100 Subject: [PATCH] code re-organization --- src/constants.ts | 8 ++ src/game.ts | 9 ++ src/layers.ts | 20 ++-- src/main.ts | 191 ++---------------------------------- src/services/map-service.ts | 172 ++++++++++++++++++++++++++++++++ src/services/ui-service.ts | 37 +++++++ src/types/types.ts | 32 ++++++ src/utilities/geo.ts | 8 ++ src/utilities/layers.ts | 19 ++++ 9 files changed, 299 insertions(+), 197 deletions(-) create mode 100644 src/game.ts create mode 100644 src/services/map-service.ts create mode 100644 src/services/ui-service.ts create mode 100644 src/utilities/layers.ts diff --git a/src/constants.ts b/src/constants.ts index 7565406..71b0515 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1 +1,9 @@ +/** + * Initial zoom level for the map + */ export const INITIAL_ZOOM = 17; + +/** + * MapTiler style URL for the map + */ +export const MAP_STYLE = import.meta.env.VITE_MAPTILER_STYLE; diff --git a/src/game.ts b/src/game.ts new file mode 100644 index 0000000..b802f35 --- /dev/null +++ b/src/game.ts @@ -0,0 +1,9 @@ +/** + * Game class for managing game state and logic + */ +class Game { + /** + * Starts the game + */ + public start = () => {}; +} diff --git a/src/layers.ts b/src/layers.ts index b29fe84..6e1758a 100644 --- a/src/layers.ts +++ b/src/layers.ts @@ -1,6 +1,10 @@ -import { FillLayerSpecification, LineLayerSpecification } from "@maptiler/sdk"; +import { FillLayerSpecification } from "@maptiler/sdk"; +/** + * Configuration for all custom map layers + */ export const LAYERS = { + /** Layer for highlighting hovered buildings */ BUILDING_HIGHLIGHT: { id: "building-highlight", type: "fill", @@ -13,6 +17,7 @@ export const LAYERS = { filter: ["==", ["id"], ""], } as FillLayerSpecification, + /** Layer for highlighting selected buildings */ BUILDING_SELECT: { id: "building-select", type: "fill", @@ -24,17 +29,4 @@ export const LAYERS = { }, filter: ["==", ["id"], ""], } as FillLayerSpecification, - - ROAD_HIGHLIGHT: { - id: "road-highlight", - type: "line", - paint: { - "line-color": "#FF0000", - "line-width": 4, - "line-opacity": 1, - }, - source: "maptiler_planet_v4", - "source-layer": "road", - filter: ["==", ["id"], ""], - } as LineLayerSpecification, }; diff --git a/src/main.ts b/src/main.ts index bd3b9c6..007537e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,18 +6,16 @@ import "@maptiler/sdk/dist/maptiler-sdk.css"; // LIBRARIES import $ from "jquery"; -import { FilterSpecification, Map, config } from "@maptiler/sdk"; +import { config } from "@maptiler/sdk"; import { createIcons, Locate, LocateFixed } from "lucide"; -import area from "@turf/area"; -import { polygon } from "@turf/helpers"; // PROJECT -import { LAYERS } from "@/layers"; -import { panToCurrentLocation, route } from "@/utilities/geo"; +import { panToCurrentLocation } from "@/utilities/geo"; +import { MapService } from "@/services/map-service"; +import { UIService } from "./services/ui-service"; // ENV const API_KEY = import.meta.env.VITE_MAPTILER_API_KEY || ""; -const MAP_STYLE = import.meta.env.VITE_MAPTILER_STYLE; createIcons({ icons: { @@ -26,187 +24,14 @@ createIcons({ }, }); -// TODO -// WEBSOCKET TBD!! -// const ws = new WebSocket(`ws://${import.meta.env.VITE_SERVER_URL}/ws`); -// ws.onopen = () => { -// console.log("opened"); -// }; -// ws.onmessage = (ev) => { -// console.log("il server dice"); -// console.log(ev.data); -// }; - config.apiKey = API_KEY; -interface SelectedFeature { - id: number; - area: number; -} - -const $info = $("#layer"); -const $lnglat = $("#lnglat"); -const $selected = $("#selected"); - -let selected: Array = []; - -const map = new Map({ - container: "map", - style: MAP_STYLE, -}); +const mapService = new MapService(new UIService()); +mapService.load(); $("#locate").on("click", (e) => { e.preventDefault(); - panToCurrentLocation(map); + panToCurrentLocation(mapService.map); }); -// Funzione helper per settare i filtri dei layer. -// Per non avere array hardcoded.] -const layerFilterEq = (id?: string | number) => { - return ["==", ["id"], id ? id : ""] as FilterSpecification; -}; -const layerFilterIn = (ids: Array) => { - return ["in", ["id"], ["literal", ids]] as FilterSpecification; -}; - -map.on("load", () => { - // Aggiunge tutti i layer custom a codice per le interazioni - map.addLayer(LAYERS.BUILDING_HIGHLIGHT); - map.addLayer(LAYERS.BUILDING_SELECT); - map.addLayer(LAYERS.ROAD_HIGHLIGHT); - - // Handle map hover. - map.on("mousemove", (e) => { - // Prende le features solo di questi livelli - const features = map.queryRenderedFeatures(e.point, { - layers: ["building_matteo", "road_network"], - }); - - // Aggiorna latitudine e longitudine nel container al mouse over. - $lnglat.html(`${e.lngLat.lng}
${e.lngLat.lat}`); - - if (features && features.length > 0) { - // Prendo l'ID della top level feature in hover - const hovered = features[0]; - const hoveredId = hovered.id; - - // Aggiorna l'ID della feature nel container delle informazioni. - $info.html(hoveredId?.toString() || ""); - - if (hoveredId) { - map.setFilter(LAYERS.BUILDING_HIGHLIGHT.id, layerFilterEq(hoveredId)); - // map.setFilter(LAYERS.ROAD_HIGHLIGHT.id, layerFilterEq(hoveredId)); - map.getCanvas().style.cursor = "pointer"; - } - } else { - map.getCanvas().style.cursor = "default"; - map.setFilter(LAYERS.BUILDING_HIGHLIGHT.id, layerFilterEq()); - // map.setFilter(LAYERS.ROAD_HIGHLIGHT.id, layerFilterEq()); - } - }); - - // Handle map click. - map.on("click", (e) => { - const features = map.queryRenderedFeatures(e.point, { - layers: ["building_matteo"], - }); - - if (features && features.length > 0) { - const clicked = features[0]; - const clickedId: number = - typeof clicked.id == "string" ? parseInt(clicked.id) : clicked.id || 0; - let clickedArea: number = 0.0; - - // TODO - // WEBSOCKET TBD! - // ws.send( - // JSON.stringify({ - // clicked: clickedId, - // }), - // ); - - // Calculate the area of the selected. - if ( - clicked.geometry.type == "Polygon" && - clicked.geometry.coordinates.length > 0 - ) { - let clickedCoords = clicked.geometry.coordinates[0]; - let clickedPolygon = polygon([clickedCoords]); - clickedArea = Math.trunc(area(clickedPolygon)); - } - - // Handle "selected list" with deletion. - const i = selected.findIndex((u) => u.id == clickedId); - if (i !== -1) { - selected.splice(i, 1); - } else { - selected.push({ - id: clickedId, - area: clickedArea, - }); - } - - // Update the "selected" list. - const selectedList = $("
    "); - selected.forEach((feature) => { - selectedList.append( - `
  • ${feature.id} (${feature.area} m2)
  • `, - ); - }); - $selected.empty().append(selectedList); - - if (selected.length === 0) { - $selected.append("
    None
    "); - } - - // Sets the selected layer. - if (clickedId) { - map.setFilter(LAYERS.BUILDING_HIGHLIGHT.id, layerFilterEq()); - map.setFilter( - LAYERS.BUILDING_SELECT.id, - layerFilterIn( - selected.map((s) => { - return s.id; - }), - ), - ); - map.getCanvas().style.cursor = "pointer"; - } - } else { - map.getCanvas().style.cursor = "default"; - } - }); - - // PROVA ROUTE - route( - "routed-foot", - "12.234413623809816,45.486327517344776", - "12.228652238845827,45.48756107353653", - ).then((res) => { - map.addSource("route", { - type: "geojson", - data: { - type: "Feature", - properties: {}, - geometry: res.routes[0].geometry as GeoJSON.Geometry, - }, - }); - - map.addLayer({ - id: "prova", - type: "line", - source: "route", - layout: { - "line-join": "round", - "line-cap": "round", - }, - paint: { - "line-color": "#FF0000", // Colore blu - "line-width": 5, // Spessore in pixel - "line-opacity": 0.8, - }, - }); - }); -}); - -panToCurrentLocation(map); +panToCurrentLocation(mapService.map); diff --git a/src/services/map-service.ts b/src/services/map-service.ts new file mode 100644 index 0000000..b8ba243 --- /dev/null +++ b/src/services/map-service.ts @@ -0,0 +1,172 @@ +// LIBRARIES +import { Map } from "@maptiler/sdk"; +import { polygon } from "@turf/helpers"; +import area from "@turf/area"; + +// PROJECT +import { SelectedFeature } from "../types/types"; +import { MAP_STYLE } from "@/constants"; +import { LAYERS } from "../layers"; +import { layerFilterEq, layerFilterIn } from "@/utilities/layers"; +import { UIService } from "./ui-service"; + +/** + * Service for managing map interactions and state + */ +export class MapService { + private uiService: UIService; + private selected: Array = []; + public map: Map; + + /** + * Creates a new MapService instance + * @param uiService - The UI service for updating interface elements + */ + public constructor(uiService: UIService) { + this.uiService = uiService; + + this.map = new Map({ + container: "map", + style: MAP_STYLE, + }); + } + + /** + * Loads the map and initializes all event listeners + */ + public load() { + this.map.on("load", () => { + // Aggiunge tutti i layer custom a codice per le interazioni + this.map.addLayer(LAYERS.BUILDING_HIGHLIGHT); + this.map.addLayer(LAYERS.BUILDING_SELECT); + + // Handle map hover. + this.map.on("mousemove", (e) => { + // Prende le features solo di questi livelli + const features = this.map.queryRenderedFeatures(e.point, { + layers: ["building_matteo", "road_network"], + }); + + // Aggiorna latitudine e longitudine nel container al mouse over. + this.uiService.$lnglat.html(`${e.lngLat.lng}
    ${e.lngLat.lat}`); + + if (features && features.length > 0) { + // Prendo l'ID della top level feature in hover + const hovered = features[0]; + const hoveredId = hovered.id; + + // Aggiorna l'ID della feature nel container delle informazioni. + this.uiService.$info.html(hoveredId?.toString() || ""); + + if (hoveredId) { + this.map.setFilter( + LAYERS.BUILDING_HIGHLIGHT.id, + layerFilterEq(hoveredId), + ); + this.map.getCanvas().style.cursor = "pointer"; + } + } else { + this.map.getCanvas().style.cursor = "default"; + this.map.setFilter(LAYERS.BUILDING_HIGHLIGHT.id, layerFilterEq()); + } + }); + + this.handleClicks(); + + // PROVA ROUTE + // route( + // "routed-foot", + // "12.234413623809816,45.486327517344776", + // "12.228652238845827,45.48756107353653", + // ).then((res) => { + // map.addSource("route", { + // type: "geojson", + // data: { + // type: "Feature", + // properties: {}, + // geometry: res.routes[0].geometry as GeoJSON.Geometry, + // }, + // }); + + // map.addLayer({ + // id: "prova", + // type: "line", + // source: "route", + // layout: { + // "line-join": "round", + // "line-cap": "round", + // }, + // paint: { + // "line-color": "#FF0000", // Colore blu + // "line-width": 5, // Spessore in pixel + // "line-opacity": 0.8, + // }, + // }); + // }); + }); + } + + /** + * Handles click events on map features + */ + public handleClicks() { + this.map.on("click", (e) => { + const features = this.map.queryRenderedFeatures(e.point, { + layers: ["building_matteo"], + }); + + if (features && features.length > 0) { + const clicked = features[0]; + const clickedId: number = + typeof clicked.id == "string" + ? parseInt(clicked.id) + : clicked.id || 0; + let clickedArea: number = 0.0; + + // Calculate the area of the selected. + if ( + clicked.geometry.type == "Polygon" && + clicked.geometry.coordinates.length > 0 + ) { + let clickedCoords = clicked.geometry.coordinates[0]; + let clickedPolygon = polygon([clickedCoords]); + clickedArea = Math.trunc(area(clickedPolygon)); + } + + // Handle "selected list" with deletion. + const i = this.selected.findIndex((u) => u.id == clickedId); + if (i !== -1) { + this.selected.splice(i, 1); + } else { + this.selected.push({ + id: clickedId, + area: clickedArea, + }); + } + + // Update the "selected" list. + this.uiService.updateSelectedList(this.selected); + + if (this.selected.length === 0) { + this.uiService.$selected.append("
    None
    "); + } + + // Sets the selected layer. + if (clickedId) { + this.map.setFilter(LAYERS.BUILDING_HIGHLIGHT.id, layerFilterEq()); + this.map.setFilter( + LAYERS.BUILDING_SELECT.id, + layerFilterIn( + this.selected.map((s) => { + return s.id; + }), + ), + ); + this.map.getCanvas().style.cursor = "pointer"; + } + } else { + this.map.getCanvas().style.cursor = "default"; + } + }); + } +} diff --git a/src/services/ui-service.ts b/src/services/ui-service.ts new file mode 100644 index 0000000..38f0c2e --- /dev/null +++ b/src/services/ui-service.ts @@ -0,0 +1,37 @@ +// LIBRARIES +import $ from "jquery"; + +// PROJECT +import { SelectedFeature } from "@/types/types"; + +/** + * Service for managing UI updates and DOM manipulation + */ +export class UIService { + public $info: JQuery; + public $lnglat: JQuery; + public $selected: JQuery; + + /** + * Creates a new UIService instance and initializes jQuery selectors + */ + public constructor() { + this.$info = $("#layer"); + this.$lnglat = $("#lnglat"); + this.$selected = $("#selected"); + } + + /** + * Updates the selected features list in the UI + * @param selected - Array of selected features to display + */ + public updateSelectedList(selected: SelectedFeature[]) { + const selectedList = $("
      "); + selected.forEach((feature) => { + selectedList.append( + `
    • ${feature.id} (${feature.area} m2)
    • `, + ); + }); + this.$selected.empty().append(selectedList); + } +} diff --git a/src/types/types.ts b/src/types/types.ts index 7206a91..1b99c22 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,9 +1,15 @@ +/** + * Represents a route calculated by the routing service + */ export interface Route { code: string; routes: RouteElement[]; waypoints: Waypoint[]; } +/** + * Represents a single route option in the routing response + */ export interface RouteElement { legs: Leg[]; weight_name: string; @@ -13,11 +19,17 @@ export interface RouteElement { distance: number; } +/** + * GeoJSON geometry representation + */ export interface Geometry { coordinates: Array; type: string; } +/** + * Represents a leg of a route between two waypoints + */ export interface Leg { steps: Step[]; weight: number; @@ -26,6 +38,9 @@ export interface Leg { distance: number; } +/** + * Represents a single step in navigation instructions + */ export interface Step { intersections: Intersection[]; driving_side: string; @@ -38,6 +53,9 @@ export interface Step { distance: number; } +/** + * Represents an intersection point in the route + */ export interface Intersection { out?: number; entry: boolean[]; @@ -46,6 +64,9 @@ export interface Intersection { in?: number; } +/** + * Represents a maneuver instruction at a specific location + */ export interface Maneuver { bearing_after: number; bearing_before: number; @@ -54,9 +75,20 @@ export interface Maneuver { modifier?: string; } +/** + * Represents a waypoint in the route + */ export interface Waypoint { hint: string; location: number[]; name: string; distance: number; } + +/** + * Represents a feature selected on the map + */ +export interface SelectedFeature { + id: number; + area: number; +} diff --git a/src/utilities/geo.ts b/src/utilities/geo.ts index 4b75199..2606101 100644 --- a/src/utilities/geo.ts +++ b/src/utilities/geo.ts @@ -14,6 +14,14 @@ export const panToCurrentLocation = (map: Map) => { }); }; +/** + * Calculates a route between two points using the specified transport mode + * @param transport - The transport mode to use for routing + * @param from - Starting coordinate in "lng,lat" format + * @param to - Ending coordinate in "lng,lat" format + * @returns A Promise that resolves to the route information + * @throws Error if the routing API request fails + */ export const route = async ( transport: Transport, from: string, diff --git a/src/utilities/layers.ts b/src/utilities/layers.ts new file mode 100644 index 0000000..558c0fe --- /dev/null +++ b/src/utilities/layers.ts @@ -0,0 +1,19 @@ +import { FilterSpecification } from "@maptiler/sdk"; + +/** + * Creates an equality filter for Mapbox layer filtering + * @param id - The feature ID to match + * @returns A FilterSpecification for the given ID + */ +export const layerFilterEq = (id?: string | number) => { + return ["==", ["id"], id ? id : ""] as FilterSpecification; +}; + +/** + * Creates an inclusion filter for multiple feature IDs + * @param ids - Array of feature IDs to include in the filter + * @returns A FilterSpecification for the given IDs + */ +export const layerFilterIn = (ids: Array) => { + return ["in", ["id"], ["literal", ids]] as FilterSpecification; +};