diff --git a/app/src/pages/MapContainer.tsx b/app/src/pages/MapContainer.tsx index b30c1cb7..3599a888 100644 --- a/app/src/pages/MapContainer.tsx +++ b/app/src/pages/MapContainer.tsx @@ -86,6 +86,8 @@ function MapContainer({ layers, map }: { layers: LayerProps[]; map: any }) { expandLayerControl={map.expand_layer_control} tileServerUrl={map.tile_server_url} tileServerAttribution={map.tile_server_attribution} + tilesType={map.tiles_type} + maplibreStyle={map.maplibre_style} showFullscreenControl={map.show_fullscreen_control} > {layers && diff --git a/backend/directus-config/development/snapshot/fields/maps/maplibre.json b/backend/directus-config/development/snapshot/fields/maps/maplibre.json new file mode 100644 index 00000000..5f4ab619 --- /dev/null +++ b/backend/directus-config/development/snapshot/fields/maps/maplibre.json @@ -0,0 +1,46 @@ +{ + "collection": "maps", + "field": "maplibre", + "type": "alias", + "meta": { + "collection": "maps", + "conditions": [ + { + "hidden": false, + "name": "Show when maplibre tiles selected", + "options": null, + "rule": { + "_and": [ + { + "tiles_type": { + "_eq": "maplibre" + } + } + ] + } + } + ], + "display": null, + "display_options": null, + "field": "maplibre", + "group": "tile_server", + "hidden": true, + "interface": "group-detail", + "note": "Configuration for MapLibre GL vector tiles", + "options": { + "start": "open" + }, + "readonly": false, + "required": false, + "sort": 3, + "special": [ + "alias", + "no-data", + "group" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + } +} diff --git a/backend/directus-config/development/snapshot/fields/maps/maplibre_style.json b/backend/directus-config/development/snapshot/fields/maps/maplibre_style.json new file mode 100644 index 00000000..cd5f0c89 --- /dev/null +++ b/backend/directus-config/development/snapshot/fields/maps/maplibre_style.json @@ -0,0 +1,45 @@ +{ + "collection": "maps", + "field": "maplibre_style", + "type": "string", + "meta": { + "collection": "maps", + "conditions": null, + "display": null, + "display_options": null, + "field": "maplibre_style", + "group": "maplibre", + "hidden": false, + "interface": "input", + "note": "MapLibre style URL (default: OpenFreeMap Liberty style)", + "options": { + "placeholder": "https://tiles.openfreemap.org/styles/liberty" + }, + "readonly": false, + "required": false, + "sort": 1, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "maplibre_style", + "table": "maps", + "data_type": "character varying", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/backend/directus-config/development/snapshot/fields/maps/raster_tiles.json b/backend/directus-config/development/snapshot/fields/maps/raster_tiles.json new file mode 100644 index 00000000..abab9b57 --- /dev/null +++ b/backend/directus-config/development/snapshot/fields/maps/raster_tiles.json @@ -0,0 +1,46 @@ +{ + "collection": "maps", + "field": "raster_tiles", + "type": "alias", + "meta": { + "collection": "maps", + "conditions": [ + { + "hidden": false, + "name": "Show when raster tiles selected", + "options": null, + "rule": { + "_and": [ + { + "tiles_type": { + "_eq": "raster" + } + } + ] + } + } + ], + "display": null, + "display_options": null, + "field": "raster_tiles", + "group": "tile_server", + "hidden": true, + "interface": "group-detail", + "note": "Configuration for raster tile layers", + "options": { + "start": "open" + }, + "readonly": false, + "required": false, + "sort": 2, + "special": [ + "alias", + "no-data", + "group" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + } +} diff --git a/backend/directus-config/development/snapshot/fields/maps/tile_server_attribution.json b/backend/directus-config/development/snapshot/fields/maps/tile_server_attribution.json index dd6e5403..86025949 100644 --- a/backend/directus-config/development/snapshot/fields/maps/tile_server_attribution.json +++ b/backend/directus-config/development/snapshot/fields/maps/tile_server_attribution.json @@ -8,11 +8,13 @@ "display": null, "display_options": null, "field": "tile_server_attribution", - "group": "tile_server", + "group": "raster_tiles", "hidden": false, "interface": "input", - "note": null, - "options": null, + "note": "Attribution text for raster tiles", + "options": { + "placeholder": "© OpenStreetMap" + }, "readonly": false, "required": false, "sort": 2, diff --git a/backend/directus-config/development/snapshot/fields/maps/tile_server_url.json b/backend/directus-config/development/snapshot/fields/maps/tile_server_url.json index cd7aea26..15bdb814 100644 --- a/backend/directus-config/development/snapshot/fields/maps/tile_server_url.json +++ b/backend/directus-config/development/snapshot/fields/maps/tile_server_url.json @@ -8,11 +8,13 @@ "display": null, "display_options": null, "field": "tile_server_url", - "group": "tile_server", + "group": "raster_tiles", "hidden": false, "interface": "input", - "note": null, - "options": null, + "note": "Raster tile server URL template (e.g., https://tile.osmand.net/hd/{z}/{x}/{y}.png)", + "options": { + "placeholder": "https://tile.osmand.net/hd/{z}/{x}/{y}.png" + }, "readonly": false, "required": false, "sort": 1, diff --git a/backend/directus-config/development/snapshot/fields/maps/tiles_type.json b/backend/directus-config/development/snapshot/fields/maps/tiles_type.json new file mode 100644 index 00000000..c75b3f39 --- /dev/null +++ b/backend/directus-config/development/snapshot/fields/maps/tiles_type.json @@ -0,0 +1,54 @@ +{ + "collection": "maps", + "field": "tiles_type", + "type": "string", + "meta": { + "collection": "maps", + "conditions": null, + "display": null, + "display_options": null, + "field": "tiles_type", + "group": "tile_server", + "hidden": false, + "interface": "select-dropdown", + "note": "Choose between raster tiles or vector tiles (MapLibre GL)", + "options": { + "choices": [ + { + "text": "Raster Tiles", + "value": "raster" + }, + { + "text": "Vector Tiles (MapLibre GL)", + "value": "maplibre" + } + ] + }, + "readonly": false, + "required": false, + "sort": 1, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "tiles_type", + "table": "maps", + "data_type": "character varying", + "default_value": "raster", + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/lib/package.json b/lib/package.json index 6cb120b2..df6b3908 100644 --- a/lib/package.json +++ b/lib/package.json @@ -77,7 +77,7 @@ "eslint-plugin-react-refresh": "^0.4.18", "eslint-plugin-security": "^3.0.1", "eslint-plugin-yml": "^1.14.0", - "happy-dom": "^16.8.1", + "happy-dom": "^20.0.0", "postcss": "^8.4.21", "prettier": "^3.3.3", "react": "^18.3.1", @@ -102,6 +102,7 @@ }, "dependencies": { "@heroicons/react": "^2.0.17", + "@maplibre/maplibre-gl-leaflet": "^0.1.3", "@tanstack/react-query": "^5.17.8", "@tiptap/core": "^3.6.5", "@tiptap/extension-bubble-menu": "^3.6.5", @@ -119,6 +120,7 @@ "date-fns": "^3.3.1", "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.79.0", + "maplibre-gl": "^5.9.0", "radash": "^12.1.0", "react-colorful": "^5.6.1", "react-dropzone": "^14.3.8", diff --git a/lib/src/Components/Map/Subcomponents/MapLibreLayer.tsx b/lib/src/Components/Map/Subcomponents/MapLibreLayer.tsx new file mode 100644 index 00000000..7fc220b2 --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/MapLibreLayer.tsx @@ -0,0 +1,53 @@ +/* eslint-disable import/no-unassigned-import */ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import L from 'leaflet' +import { useEffect } from 'react' +import { useMap } from 'react-leaflet' + +import '@maplibre/maplibre-gl-leaflet' +import 'maplibre-gl/dist/maplibre-gl.css' + +// Augment Leaflet namespace with MapLibre GL types +declare module 'leaflet' { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function maplibreGL(options: { style: string; attribution?: string }): any +} + +/** + * MapLibreLayer component for rendering vector tiles with MapLibre GL + * Integrates MapLibre GL with Leaflet using the maplibre-gl-leaflet bridge + * + * @param styleUrl - URL to the MapLibre style JSON (default: OpenFreeMap Liberty style) + * @param attribution - Attribution text for the map tiles + */ +export function MapLibreLayer({ + styleUrl = 'https://tiles.openfreemap.org/styles/liberty', + attribution = '© OpenStreetMap', +}: { + styleUrl?: string + attribution?: string +}) { + const map = useMap() + + useEffect(() => { + // Create MapLibre GL layer + const mapLibreLayer = L.maplibreGL({ + style: styleUrl, + attribution, + }) + + // Add layer to map + mapLibreLayer.addTo(map) + + // Cleanup function to remove layer when component unmounts + return () => { + map.removeLayer(mapLibreLayer) + } + }, [map, styleUrl, attribution]) + + return null +} diff --git a/lib/src/Components/Map/UtopiaMap.tsx b/lib/src/Components/Map/UtopiaMap.tsx index 939cf424..70bd695f 100644 --- a/lib/src/Components/Map/UtopiaMap.tsx +++ b/lib/src/Components/Map/UtopiaMap.tsx @@ -59,6 +59,8 @@ function UtopiaMap({ expandLayerControl, tileServerUrl, tileServerAttribution, + tilesType = 'raster', + maplibreStyle, }: { /** height of the map (default '500px') */ height?: string @@ -94,6 +96,10 @@ function UtopiaMap({ tileServerUrl?: string /** configure a custom tile server attribution */ tileServerAttribution?: string + /** tiles type: 'raster' or 'maplibre' (default 'raster') */ + tilesType?: 'raster' | 'maplibre' + /** MapLibre style URL for vector tiles (default: OpenFreeMap Liberty) */ + maplibreStyle?: string }) { return ( @@ -116,6 +122,8 @@ function UtopiaMap({ expandLayerControl={expandLayerControl} tileServerUrl={tileServerUrl} tileServerAttribution={tileServerAttribution} + tilesType={tilesType} + maplibreStyle={maplibreStyle} > {children} diff --git a/lib/src/Components/Map/UtopiaMapInner.tsx b/lib/src/Components/Map/UtopiaMapInner.tsx index 9b102a93..0a550e56 100644 --- a/lib/src/Components/Map/UtopiaMapInner.tsx +++ b/lib/src/Components/Map/UtopiaMapInner.tsx @@ -42,6 +42,7 @@ import { LayerControl } from './Subcomponents/Controls/LayerControl' import { SearchControl } from './Subcomponents/Controls/SearchControl' import { TagsControl } from './Subcomponents/Controls/TagsControl' import { TextView } from './Subcomponents/ItemPopupComponents/TextView' +import { MapLibreLayer } from './Subcomponents/MapLibreLayer' import { SelectPosition } from './Subcomponents/SelectPosition' import type { Feature, Geometry as GeoJSONGeometry, GeoJsonObject } from 'geojson' @@ -59,6 +60,8 @@ export function UtopiaMapInner({ expandLayerControl, tileServerUrl, tileServerAttribution, + tilesType, + maplibreStyle, }: { children?: React.ReactNode geo?: GeoJsonObject @@ -72,6 +75,8 @@ export function UtopiaMapInner({ expandLayerControl?: boolean tileServerUrl?: string tileServerAttribution?: string + tilesType?: 'raster' | 'maplibre' + maplibreStyle?: string }) { const selectNewItemPosition = useSelectPosition() const setSelectNewItemPosition = useSetSelectPosition() @@ -284,14 +289,18 @@ export function UtopiaMapInner({ {showLayerControl && } {showGratitudeControl && } - OpenStreetMap' - } - url={tileServerUrl ?? 'https://tile.osmand.net/hd/{z}/{x}/{y}.png'} - /> + {tilesType === 'raster' ? ( + OpenStreetMap' + } + url={tileServerUrl ?? 'https://tile.osmand.net/hd/{z}/{x}/{y}.png'} + /> + ) : ( + + )} setClusterRef(r as any)} showCoverageOnHover