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