code re-organization
This commit is contained in:
@@ -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
9
src/game.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Game class for managing game state and logic
|
||||
*/
|
||||
class Game {
|
||||
/**
|
||||
* Starts the game
|
||||
*/
|
||||
public start = () => {};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
191
src/main.ts
191
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<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
172
src/services/map-service.ts
Normal 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";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
37
src/services/ui-service.ts
Normal file
37
src/services/ui-service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
19
src/utilities/layers.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user