code re-organization

This commit is contained in:
Matteo Rosati
2026-01-25 09:27:50 +01:00
parent 3f3509bfea
commit 73b3407f04
9 changed files with 299 additions and 197 deletions

View File

@@ -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;

9
src/game.ts Normal file
View File

@@ -0,0 +1,9 @@
/**
* Game class for managing game state and logic
*/
class Game {
/**
* Starts the game
*/
public start = () => {};
}

View File

@@ -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,
};

View File

@@ -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<SelectedFeature> = [];
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<string | number | undefined>) => {
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}<br>${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 = $("<ul>");
selected.forEach((feature) => {
selectedList.append(
`<li>${feature.id} (${feature.area} m<sup>2</sup>)</li>`,
);
});
$selected.empty().append(selectedList);
if (selected.length === 0) {
$selected.append("<div>None</div>");
}
// 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);

172
src/services/map-service.ts Normal file
View File

@@ -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<SelectedFeature> = [];
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}<br>${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("<div>None</div>");
}
// 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";
}
});
}
}

View File

@@ -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 = $("<ul>");
selected.forEach((feature) => {
selectedList.append(
`<li>${feature.id} (${feature.area} m<sup>2</sup>)</li>`,
);
});
this.$selected.empty().append(selectedList);
}
}

View File

@@ -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<number[]>;
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;
}

View File

@@ -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,

19
src/utilities/layers.ts Normal file
View File

@@ -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<string | number | undefined>) => {
return ["in", ["id"], ["literal", ids]] as FilterSpecification;
};