@premiate/strapi-plugin-maplibre-field 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @premiate/strapi-plugin-maplibre-field might be problematic. Click here for more details.

Files changed (47) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +23 -0
  3. package/README.md +781 -0
  4. package/dist/_chunks/de-CGU2cyif.mjs +16 -0
  5. package/dist/_chunks/de-Dq_t3Z6M.js +16 -0
  6. package/dist/_chunks/en-BxxNWf9i.mjs +16 -0
  7. package/dist/_chunks/en-CgSPA-1L.js +16 -0
  8. package/dist/_chunks/es-B_cPv3G5.mjs +16 -0
  9. package/dist/_chunks/es-Sgja1XAa.js +16 -0
  10. package/dist/_chunks/fr-B3JIzyzo.js +16 -0
  11. package/dist/_chunks/fr-Dw5wEoDC.mjs +16 -0
  12. package/dist/_chunks/index-BF5T-kqa.mjs +171 -0
  13. package/dist/_chunks/index-BNnkn7JG.mjs +1778 -0
  14. package/dist/_chunks/index-CGJogtZr.js +1799 -0
  15. package/dist/_chunks/index-nbk0hg-O.js +170 -0
  16. package/dist/_chunks/it-BgWDIXzn.js +16 -0
  17. package/dist/_chunks/it-CoUEVPt6.mjs +16 -0
  18. package/dist/admin/index.js +3 -0
  19. package/dist/admin/index.mjs +4 -0
  20. package/dist/admin/src/components/Initializer.d.ts +6 -0
  21. package/dist/admin/src/components/MapInput/basemap-control.d.ts +8 -0
  22. package/dist/admin/src/components/MapInput/credits-control.d.ts +10 -0
  23. package/dist/admin/src/components/MapInput/geocoder-control.d.ts +20 -0
  24. package/dist/admin/src/components/MapInput/index.d.ts +19 -0
  25. package/dist/admin/src/components/MapInput/layer-control.d.ts +18 -0
  26. package/dist/admin/src/components/PluginIcon.d.ts +2 -0
  27. package/dist/admin/src/hooks/usePluginConfig.d.ts +2 -0
  28. package/dist/admin/src/index.d.ts +16 -0
  29. package/dist/admin/src/mutations/mutateEditViewHook.d.ts +30 -0
  30. package/dist/admin/src/services/poi-service.d.ts +160 -0
  31. package/dist/admin/src/utils/getTrad.d.ts +2 -0
  32. package/dist/admin/src/utils/pluginId.d.ts +2 -0
  33. package/dist/admin/src/utils/prefixPluginTranslations.d.ts +3 -0
  34. package/dist/server/index.js +107 -0
  35. package/dist/server/index.mjs +108 -0
  36. package/dist/server/src/bootstrap.d.ts +2 -0
  37. package/dist/server/src/config/index.d.ts +61 -0
  38. package/dist/server/src/config/schema.d.ts +58 -0
  39. package/dist/server/src/controllers/config.d.ts +7 -0
  40. package/dist/server/src/controllers/index.d.ts +8 -0
  41. package/dist/server/src/destroy.d.ts +5 -0
  42. package/dist/server/src/index.d.ts +85 -0
  43. package/dist/server/src/register.d.ts +5 -0
  44. package/dist/server/src/routes/index.d.ts +9 -0
  45. package/dist/server/src/types/config.d.ts +26 -0
  46. package/logo.png +0 -0
  47. package/package.json +99 -0
package/README.md ADDED
@@ -0,0 +1,781 @@
1
+ # MapLibre Field - Strapi v5 Plugin
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@premiate/strapi-plugin-maplibre-field)](https://www.npmjs.com/package/@premiate/strapi-plugin-maplibre-field)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+ [![Strapi v5](https://img.shields.io/badge/Strapi-v5-blue)](https://strapi.io)
6
+
7
+ A [Strapi](https://strapi.io/) plugin that provides a [MapLibre](https://www.maplibre.org/) map custom field for your content-types, allowing for mutiple base maps and multiple POI layers configuration, storing GeoJSON Features behind the scene.
8
+
9
+ ![Map Field](https://codeberg.org/Premiate-Edizioni/strapi-plugin-maplibre-field/raw/branch/main/add-or-pin-on-map.png)
10
+
11
+ You can use the search box to pinpoint the location you are looking for. Alternatively, you can double-click anywhere on the map, which will put a marker at the exact point and set longitude and latitude. Any Point Of Interest on the base map or while setting the address at the closest geolocated point on OpenStreetMap.
12
+
13
+ The longitude and latitude of the geolocated point are displayed in the readonly fields underneath the map. The address matches the closest geolocated point.
14
+
15
+ The field is stored as a standard [GeoJSON Feature](https://geojson.org/) (RFC 7946) in a JSON field:
16
+
17
+ ```json
18
+ {
19
+ "type": "Feature",
20
+ "geometry": {
21
+ "type": "Point",
22
+ "coordinates": [9.195433, 45.464181]
23
+ },
24
+ "properties": {
25
+ "name": "Comando Polizia Locale",
26
+ "address": "Piazza Cesare Beccaria, Duomo, Municipio 1, Milano, 20122, Italia",
27
+ "source": "nominatim",
28
+ "sourceId": "nominatim-12345678",
29
+ "category": "police",
30
+ "inputMethod": "search"
31
+ }
32
+ }
33
+ ```
34
+
35
+ See [Data Model](#data-model) for details on all properties.
36
+
37
+ ## Table of Contents
38
+
39
+ - [Installation](#installation)
40
+ - [Configuration](#configuration)
41
+ - [Add a map field](#add-a-map-field-to-your-content-type)
42
+ - [Features](#features)
43
+ - [POI Selection](#poi-point-of-interest-selection)
44
+ - [Data Model](#data-model)
45
+ - [Contributing](#contributing)
46
+ - [Credits](#credits)
47
+ - [License](#license)
48
+
49
+ ## Installation
50
+
51
+ ### Requirements
52
+
53
+ - Strapi v5.0.0 or higher
54
+ - Node.js 20.0.0 or higher
55
+
56
+ ### Install
57
+
58
+ ```sh
59
+ # Using Yarn
60
+ yarn add strapi-plugin-maplibre-field
61
+
62
+ # Or using NPM
63
+ npm install strapi-plugin-maplibre-field
64
+ ```
65
+
66
+ ## Configuration
67
+
68
+ ### Enable the plugin
69
+
70
+ Create or update `config/plugins.js` (or `config/plugins.ts` for TypeScript):
71
+
72
+ ```typescript
73
+ // config/plugins.ts
74
+ export default {
75
+ "maplibre-field": {
76
+ enabled: true,
77
+ config: {
78
+ // Optional: Customize map settings
79
+ mapStyles: [
80
+ {
81
+ id: "satellite",
82
+ name: "Satellite",
83
+ url: "https://api.maptiler.com/maps/satellite-v4/style.json?key=YOUR_API_KEY",
84
+ isDefault: true,
85
+ },
86
+ {
87
+ id: "osm",
88
+ name: "OpenStreetMap",
89
+ url: "https://api.maptiler.com/maps/openstreetmap/style.json?key=YOUR_API_KEY",
90
+ },
91
+ ],
92
+ defaultZoom: 4.5,
93
+ defaultCenter: [9.19, 45.46], // [longitude, latitude] - Milano, Italy
94
+ geocodingProvider: "nominatim",
95
+ nominatimUrl: "https://nominatim.openstreetmap.org",
96
+ },
97
+ },
98
+ };
99
+ ```
100
+
101
+ ### Configuration Options
102
+
103
+ | Option | Type | Default | Description |
104
+ | ------------------- | ------------------ | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
105
+ | `mapStyles` | `MapStyle[]` | See default config | Array of map style configurations. The map will open with the first style or the one marked as `isDefault: true` |
106
+ | `defaultZoom` | `number` | `4.5` | Initial zoom level (0-20) |
107
+ | `defaultCenter` | `[number, number]` | `[0, 0]` | Initial map center [longitude, latitude] |
108
+ | `geocodingProvider` | `string` | `'nominatim'` | Geocoding service provider |
109
+ | `nominatimUrl` | `string` | `'https://nominatim.openstreetmap.org'` | Nominatim API endpoint |
110
+
111
+ #### MapStyle Interface
112
+
113
+ Each map style in the `mapStyles` array has the following structure:
114
+
115
+ ```typescript
116
+ interface MapStyle {
117
+ id: string; // Unique identifier for the style
118
+ name: string; // Display name shown in the basemap switcher
119
+ url: string; // URL to MapLibre style JSON
120
+ isDefault?: boolean; // Set to true for the default style (optional)
121
+ }
122
+ ```
123
+
124
+ ### Map Style Options
125
+
126
+ The plugin uses **MapLibre GL JS** for rendering, which supports any style that follows the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/).
127
+
128
+ #### Default Configuration
129
+
130
+ By default, the plugin includes multiple map styles:
131
+
132
+ - **Satellite** view using MapTiler satellite imagery
133
+ - **OpenStreetMap** view with vector tiles
134
+ - Users can switch between styles using the basemap control in the map interface
135
+
136
+ #### Configuring Map Styles
137
+
138
+ You can configure multiple map styles that users can switch between. The map will initially open with the first style in the array, or the one marked with `isDefault: true`.
139
+
140
+ **1. MapLibre Demo Tiles** (Free, public):
141
+
142
+ ```typescript
143
+ // In config/plugins.ts
144
+ export default {
145
+ "maplibre-field": {
146
+ enabled: true,
147
+ config: {
148
+ mapStyles: [
149
+ {
150
+ id: "demo",
151
+ name: "Demo",
152
+ url: "https://demotiles.maplibre.org/style.json",
153
+ isDefault: true,
154
+ },
155
+ ],
156
+ },
157
+ },
158
+ };
159
+ ```
160
+
161
+ **2. MapTiler** (Requires API key):
162
+
163
+ ```typescript
164
+ // In Strapi's config/plugins.ts
165
+ module.exports = ({ env }) => ({
166
+ "maplibre-field": {
167
+ enabled: true,
168
+ config: {
169
+ mapStyles: [
170
+ {
171
+ id: "streets",
172
+ name: "Streets",
173
+ url: `https://api.maptiler.com/maps/streets-v2/style.json?key=${env(
174
+ "MAPTILER_API_KEY"
175
+ )}`,
176
+ isDefault: true,
177
+ },
178
+ {
179
+ id: "outdoor",
180
+ name: "Outdoor",
181
+ url: `https://api.maptiler.com/maps/outdoor-v2/style.json?key=${env(
182
+ "MAPTILER_API_KEY"
183
+ )}`,
184
+ },
185
+ {
186
+ id: "satellite",
187
+ name: "Satellite",
188
+ url: `https://api.maptiler.com/maps/satellite-v4/style.json?key=${env(
189
+ "MAPTILER_API_KEY"
190
+ )}`,
191
+ },
192
+ ],
193
+ },
194
+ },
195
+ });
196
+ ```
197
+
198
+ ```bash
199
+ # In Strapi's .env file
200
+ MAPTILER_API_KEY=your_actual_api_key_here
201
+ ```
202
+
203
+ **3. Stadia Maps** (Requires API key):
204
+
205
+ ```typescript
206
+ // In Strapi's config/plugins.ts
207
+ module.exports = ({ env }) => ({
208
+ "maplibre-field": {
209
+ enabled: true,
210
+ config: {
211
+ mapStyles: [
212
+ {
213
+ id: "alidade",
214
+ name: "Alidade Smooth",
215
+ url: `https://tiles.stadiamaps.com/styles/alidade_smooth.json?api_key=${env(
216
+ "STADIA_API_KEY"
217
+ )}`,
218
+ isDefault: true,
219
+ },
220
+ {
221
+ id: "osm",
222
+ name: "OSM Bright",
223
+ url: `https://tiles.stadiamaps.com/styles/osm_bright.json?api_key=${env(
224
+ "STADIA_API_KEY"
225
+ )}`,
226
+ },
227
+ ],
228
+ },
229
+ },
230
+ });
231
+ ```
232
+
233
+ ```bash
234
+ # In .env file
235
+ STADIA_API_KEY=your_actual_api_key_here
236
+ ```
237
+
238
+ **4. Custom Style**:
239
+
240
+ - Create your own style using [Maputnik](https://maputnik.github.io/) (visual style editor)
241
+ - Host the style JSON file on your server or object storage
242
+ - Add it to the `mapStyles` array with a unique `id` and descriptive `name`
243
+
244
+ #### Environment Variables for API Keys
245
+
246
+ To keep API keys secure and out of version control, use Strapi's built-in `env()` function:
247
+
248
+ ```typescript
249
+ // config/plugins.ts
250
+ module.exports = ({ env }) => ({
251
+ "maplibre-field": {
252
+ enabled: true,
253
+ config: {
254
+ mapStyles: [
255
+ {
256
+ id: "streets",
257
+ name: "Streets",
258
+ url: `https://api.maptiler.com/maps/streets-v4/style.json?key=${env(
259
+ "MAPTILER_API_KEY"
260
+ )}`,
261
+ isDefault: true,
262
+ },
263
+ ],
264
+ },
265
+ },
266
+ });
267
+ ```
268
+
269
+ ```bash
270
+ # .env
271
+ MAPTILER_API_KEY=your_secret_key_here
272
+ ```
273
+
274
+ **Important Notes**:
275
+
276
+ - Always use template literals (backticks) when interpolating environment variables
277
+ - The `env()` function is provided by Strapi and evaluated at server startup
278
+ - API keys for map tiles (Maptiler, Stadia, etc.) are safe to expose as they're meant for client-side use with domain restrictions
279
+ - For production, ensure all required environment variables are set in your deployment environment
280
+
281
+ #### Dependencies
282
+
283
+ The plugin includes these mapping libraries:
284
+
285
+ - **maplibre-gl** (v5.16.0): Core WebGL-based map rendering engine
286
+ - **react-map-gl** (v8.1.0): React wrapper for MapLibre GL with components
287
+ - **pmtiles** (v4.3.2): Support for cloud-native PMTiles format (efficient tile hosting without a server)
288
+ - **@maplibre/maplibre-gl-geocoder** (v1.9.4): Geocoding control for the map
289
+
290
+ ### Geocoding Configuration
291
+
292
+ The plugin uses **Nominatim** by default for geocoding (converting addresses to coordinates and vice versa).
293
+
294
+ #### Using Public Nominatim (Default)
295
+
296
+ ```typescript
297
+ geocodingProvider: 'nominatim',
298
+ nominatimUrl: 'https://nominatim.openstreetmap.org',
299
+ ```
300
+
301
+ **Important**: Public Nominatim has usage limits. Please review their [Usage Policy](https://operations.osmfoundation.org/policies/nominatim/).
302
+
303
+ #### Using Self-Hosted Nominatim
304
+
305
+ For production use with high traffic, consider hosting your own Nominatim instance:
306
+
307
+ ```typescript
308
+ nominatimUrl: 'https://your-nominatim-server.org',
309
+ ```
310
+
311
+ #### Alternative Geocoding Providers
312
+
313
+ Currently, the plugin is optimized for Nominatim. Support for other providers (MapTiler, Google, etc.) can be added by extending the geocoder component.
314
+
315
+ ### Update security middleware
316
+
317
+ For the map to display properly, update the `strapi::security` middleware configuration.
318
+
319
+ Open `config/middlewares.ts` (or `.js`) and add `'worker-src': ['blob:']` to the CSP directives:
320
+
321
+ ```typescript
322
+ // config/middlewares.ts
323
+ export default [
324
+ "strapi::errors",
325
+ {
326
+ name: "strapi::security",
327
+ config: {
328
+ contentSecurityPolicy: {
329
+ useDefaults: true,
330
+ directives: {
331
+ "connect-src": ["'self'", "https:"],
332
+ "script-src": ["'self'", "'unsafe-inline'"],
333
+ "img-src": ["'self'", "data:", "blob:"],
334
+ "media-src": ["'self'", "data:", "blob:"],
335
+ "worker-src": ["blob:"], // Required for MapLibre workers
336
+ upgradeInsecureRequests: null,
337
+ },
338
+ },
339
+ },
340
+ },
341
+ "strapi::cors",
342
+ "strapi::poweredBy",
343
+ "strapi::logger",
344
+ "strapi::query",
345
+ "strapi::body",
346
+ "strapi::session",
347
+ "strapi::favicon",
348
+ "strapi::public",
349
+ ];
350
+ ```
351
+
352
+ ## Add a map field to your content type
353
+
354
+ In the Strapi content type builder:
355
+
356
+ - Click on `Add another field`
357
+ - Select the `Custom` tab
358
+ - Select the `Map` field
359
+ - Type a name for the field
360
+ - Click `Finish`
361
+
362
+ ![Add map field to content type](https://codeberg.org/Premiate-Edizioni/strapi-plugin-maplibre-field/raw/branch/main/add-maplibre-custom-field.png)
363
+
364
+ ### Localization (i18n)
365
+
366
+ By default, **localization is disabled** for map fields because geographic coordinates are universal and typically don't vary by language.
367
+
368
+ However, if you need different locations per language (e.g., different office addresses in different countries), you can enable localization in the **Advanced Settings** tab when adding the field:
369
+
370
+ 1. In the Content-Type Builder, click on the **Advanced Settings** tab
371
+ 2. Check "Enable localization for this field"
372
+
373
+ > **Note**: This setting is disabled by default as coordinates are typically the same across all languages.
374
+
375
+ ## Features
376
+
377
+ ### Interactive Map
378
+
379
+ - **MapLibre GL** powered interactive map with smooth zoom and pan
380
+ - **OpenStreetMap** tiles via Protomaps/PMTiles
381
+ - Fully customizable map style via configuration
382
+
383
+ ### Geocoding
384
+
385
+ - **Forward geocoding**: Search for places using the search box
386
+ - **Reverse geocoding**: Double-click anywhere on the map to find the nearest address
387
+ - Powered by **Nominatim** (OpenStreetMap's geocoding service)
388
+ - Real-time notifications for geocoding success, errors, and warnings
389
+
390
+ ### Multi-language Support
391
+
392
+ Built-in translations for:
393
+ - English
394
+ - German (Deutsch)
395
+ - French (Français)
396
+ - Italian (Italiano)
397
+ - Spanish (Español)
398
+
399
+ ### User Experience
400
+
401
+ - **Visual feedback**: Toast notifications for all user actions
402
+ - Success messages when addresses are found
403
+ - Warnings when no results are available
404
+ - Error messages with actionable guidance
405
+
406
+ ### Developer Experience
407
+
408
+ - **TypeScript** support
409
+ - **Configurable**: All map settings can be customized
410
+ - **Type-safe**: Proper interfaces for all API responses
411
+ - **Well-tested**: Comprehensive test coverage
412
+
413
+ ## POI (Point of Interest) Selection
414
+
415
+ The plugin supports direct POI selection with visual markers on the map, allowing users to select pre-defined locations or save coordinates.
416
+
417
+ ### POI Features
418
+
419
+ - **POI Markers Always Visible** - Shows available POIs on the map when zoomed in (customizable zoom level)
420
+ - **Direct Click Selection** - Click on any POI marker to select and save complete POI data
421
+ - **Coordinates-Only Mode** - Double-click anywhere on empty map area to save only coordinates (no name/address)
422
+ - **Custom API Support** - Integrate your own GeoJSON API alongside Nominatim
423
+ - **Enhanced Search** - Search field queries both Nominatim AND custom API, merging results with custom API priority
424
+ - **Visual Distinction** - Different marker colors configurable for custom POIs
425
+ - **Performance Optimized** - Zoom-based visibility and display limits prevent overcrowding
426
+
427
+ ### POI Configuration
428
+
429
+ Add POI configuration to your plugin settings:
430
+
431
+ ```typescript
432
+ // config/plugins.ts
433
+ export default {
434
+ "maplibre-field": {
435
+ enabled: true,
436
+ config: {
437
+ // Basic configuration
438
+ mapStyles: [
439
+ {
440
+ id: "satellite",
441
+ name: "Satellite",
442
+ url: "https://api.maptiler.com/maps/satellite-v4/style.json?key=YOUR_API_KEY",
443
+ isDefault: true,
444
+ },
445
+ ],
446
+ nominatimUrl: "https://nominatim.openstreetmap.org",
447
+ defaultCenter: [9.19, 45.46],
448
+ defaultZoom: 13,
449
+
450
+ // POI Configuration
451
+ poiDisplayEnabled: true, // Enable POI markers display
452
+ poiMinZoom: 10, // Show POIs only when zoomed in >= this level
453
+ poiMaxDisplay: 100, // Maximum number of POIs to display
454
+ poiSearchEnabled: true, // Include custom API results in search
455
+ poiSnapRadius: 5, // Snap radius in meters for double-click POI detection (default: 5m)
456
+ poiSources: [
457
+ // Example using static GeoJSON files from the public repository
458
+ {
459
+ id: "skatespots",
460
+ name: "My Skatespots",
461
+ apiUrl: "https://codeberg.org/premiate-edizioni/strapi-plugin-maplibre-field/raw/branch/main/samples/skatespots.geojson",
462
+ enabled: true,
463
+ },
464
+ {
465
+ id: "skateshops",
466
+ name: "My Skateshops",
467
+ apiUrl: "https://codeberg.org/premiate-edizioni/strapi-plugin-maplibre-field/raw/branch/main/samples/skateshops.geojson",
468
+ enabled: false,
469
+ },
470
+ ],
471
+ },
472
+ },
473
+ };
474
+ ```
475
+
476
+ ### POI Configuration Options
477
+
478
+ | Option | Type | Default | Description |
479
+ | ------------------- | ------------- | ------- | ----------------------------------------------------------------------- |
480
+ | `poiDisplayEnabled` | `boolean` | `true` | Display POI markers on map |
481
+ | `poiMinZoom` | `number` | `10` | Minimum zoom level to show POI markers (prevents overcrowding) |
482
+ | `poiMaxDisplay` | `number` | `100` | Maximum number of POIs displayed (closest to map center) |
483
+ | `poiSearchEnabled` | `boolean` | `true` | Include custom API results in search field |
484
+ | `poiSnapRadius` | `number` | `5` | Snap radius in meters for double-click POI detection |
485
+ | `poiSources` | `POISource[]` | `[]` | Array of POI sources with layer control (see POISource interface below) |
486
+
487
+ #### POISource Interface
488
+
489
+ ```typescript
490
+ interface POISource {
491
+ id: string; // Unique identifier for the layer
492
+ name: string; // Display name in layer control
493
+ apiUrl: string; // GeoJSON API endpoint URL
494
+ enabled?: boolean; // Initial layer visibility (default: true)
495
+ }
496
+ ```
497
+
498
+ **Notes:**
499
+
500
+ - `id`: Must be unique across all POI sources. Used internally to track layer state.
501
+ - `name`: Displayed in the layer control panel. Should be descriptive and concise.
502
+ - `apiUrl`: Must return a valid GeoJSON FeatureCollection (see Custom POI API section below).
503
+ - `enabled`: Controls initial visibility. Users can toggle layers on/off using the layer control panel regardless of this setting.
504
+
505
+ ### Layer Control
506
+
507
+ When multiple POI sources are configured, a **layer control panel** appears on the map allowing you to:
508
+
509
+ - **Toggle individual layers on/off**: Click the eye icon to show/hide POI markers from each source
510
+ - **Real-time updates**: POIs are automatically loaded/removed when toggling layers
511
+ - **Dynamic map movement**: When you move the map to a new area, POIs from enabled layers are automatically fetched for the new viewport
512
+ - **Independent sources**: Each POI source can be controlled separately
513
+
514
+ The layer control is automatically displayed when you configure multiple `poiSources` in your plugin configuration.
515
+
516
+ ### User Interaction
517
+
518
+ **Click on POI marker** → Selects that POI and saves complete data:
519
+
520
+ - POI name
521
+ - POI type
522
+ - Full address
523
+ - Coordinates
524
+ - Custom metadata
525
+ - Source indicator (Nominatim or custom)
526
+
527
+ **Double-click anywhere** → Intelligent POI detection with snap radius:
528
+
529
+ - **If POI found within snap radius** (default: 5m):
530
+ - Automatically selects the nearest POI
531
+ - Saves complete POI data (name, type, address, coordinates, metadata)
532
+ - Shows distance in notification (e.g., "Skatepark Milano (3m)")
533
+ - **If no POI found within snap radius**:
534
+ - Saves only coordinates (no name or address)
535
+ - Useful for marking exact locations without POI data
536
+ - **Configurable**: Adjust `poiSnapRadius` to change detection sensitivity (in meters)
537
+
538
+ **Search field** → Queries both sources:
539
+
540
+ - Nominatim results: Address Name
541
+ - Custom API results: POI Name
542
+ - Both displayed in dropdown
543
+
544
+ ### Custom POI API
545
+
546
+ The plugin supports custom GeoJSON FeatureCollection APIs for POI data.
547
+
548
+ #### Example: Fotta Skateparks API
549
+
550
+ ```
551
+ GET https://your-api-server.org/endpoint
552
+ ```
553
+
554
+ #### Response Format (GeoJSON FeatureCollection)
555
+
556
+ ```json
557
+ {
558
+ "type": "FeatureCollection",
559
+ "features": [
560
+ {
561
+ "id": "019ac1b6-5823-7808-9be6-62733b3d0a0a",
562
+ "type": "Feature",
563
+ "geometry": {
564
+ "type": "Point",
565
+ "coordinates": [11.1448496, 46.052144]
566
+ },
567
+ "properties": {
568
+ "name": "Skatepark Villa",
569
+ "sport": "skateboard",
570
+ "leisure": "pitch",
571
+ "osm_id": "node/12345",
572
+ "surface": "concrete"
573
+ }
574
+ }
575
+ ]
576
+ }
577
+ ```
578
+
579
+ #### Requirements
580
+
581
+ - **Format**: Must return GeoJSON FeatureCollection with Point features
582
+ - **Authentication**: Must be publicly accessible (no authentication)
583
+ - **Coordinates**: Must use `geometry.coordinates` as `[longitude, latitude]`
584
+ - **Name**: Must include `properties.name` (can be `null`, will use fallback)
585
+ - **Structure**: Compatible with standard GeoJSON specification
586
+
587
+ #### How It Works
588
+
589
+ 1. **Search Field**:
590
+
591
+ - Plugin queries full dataset from API
592
+ - Filters results client-side by name matching search query
593
+ - Merges with Nominatim results (both shown in dropdown)
594
+
595
+ 2. **POI Display**:
596
+
597
+ - Query POIs based on map viewport and zoom level
598
+ - Display up to `poiMaxDisplay` closest POIs to map center
599
+ - Only show when `zoom >= poiMinZoom`
600
+ - Update markers when map moves/zooms
601
+
602
+ 3. **POI Selection** (click on marker):
603
+
604
+ - Save complete POI data: name, type, address, coordinates, metadata, source
605
+ - Visual feedback with orange highlight on selected marker
606
+ - Success notification shows POI name and source
607
+
608
+ 4. **Performance**:
609
+ - API response is cached client-side (15 minutes)
610
+ - Filtering by name/viewport happens in browser
611
+ - Works well with datasets up to ~5000 features
612
+
613
+ ## Data Model
614
+
615
+ The field stores location data as a **standard GeoJSON Feature** ([RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946)). Properties are only included when available (no null or empty values).
616
+
617
+ ### GeoJSON Feature Structure
618
+
619
+ ```typescript
620
+ interface LocationFeature {
621
+ type: "Feature";
622
+ geometry: {
623
+ type: "Point";
624
+ coordinates: [number, number]; // [longitude, latitude]
625
+ };
626
+ properties: {
627
+ name?: string; // POI name or short location name
628
+ address?: string; // Full formatted address
629
+ source?: string; // "nominatim" or custom source ID (e.g., "fotta-skatespots")
630
+ sourceId?: string; // Original ID from the source
631
+ sourceLayer?: string; // Display name of the source (e.g., "Fotta Skatespots")
632
+ category?: string; // POI type/category (e.g., "skating_spot", "bus_stop")
633
+ inputMethod?: string; // How it was created: "search" | "poi_click" | "map_click"
634
+ metadata?: object; // Original metadata from the source
635
+ };
636
+ }
637
+ ```
638
+
639
+ ### Example: Nominatim Search Result
640
+
641
+ ```json
642
+ {
643
+ "type": "Feature",
644
+ "geometry": {
645
+ "type": "Point",
646
+ "coordinates": [9.1901019, 45.460068]
647
+ },
648
+ "properties": {
649
+ "name": "Piazza Velasca",
650
+ "address": "Piazza Velasca, Cerchia dei Navigli, Municipio 1, Milano, Lombardia, 20122, Italia",
651
+ "source": "nominatim",
652
+ "sourceId": "nominatim-68428992",
653
+ "category": "bus_stop",
654
+ "inputMethod": "search",
655
+ "metadata": {
656
+ "osm_id": 4843517235,
657
+ "osm_type": "node",
658
+ "place_id": 68428992,
659
+ "addresstype": "highway"
660
+ }
661
+ }
662
+ }
663
+ ```
664
+
665
+ ### Example: POI Click (Custom Source)
666
+
667
+ ```json
668
+ {
669
+ "type": "Feature",
670
+ "geometry": {
671
+ "type": "Point",
672
+ "coordinates": [9.196492, 45.480958]
673
+ },
674
+ "properties": {
675
+ "name": "Ledge Milano District",
676
+ "address": "Via Roma 1, 20121 Milano MI, Italy",
677
+ "source": "fotta-skatespots",
678
+ "sourceId": "019ac1b6-5823-7808-9be6-62733b3d0a0a",
679
+ "sourceLayer": "Fotta Skatespots",
680
+ "category": "skating_spot",
681
+ "inputMethod": "poi_click",
682
+ "metadata": {
683
+ "sport": "skateboard",
684
+ "surface": "concrete"
685
+ }
686
+ }
687
+ }
688
+ ```
689
+
690
+ ### Example: Double-Click on Empty Area
691
+
692
+ When double-clicking on an empty area (no POI within snap radius), only coordinates and input method are saved:
693
+
694
+ ```json
695
+ {
696
+ "type": "Feature",
697
+ "geometry": {
698
+ "type": "Point",
699
+ "coordinates": [9.190252, 45.459891]
700
+ },
701
+ "properties": {
702
+ "inputMethod": "map_click"
703
+ }
704
+ }
705
+ ```
706
+
707
+ ### Properties Reference
708
+
709
+ | Property | Type | Description |
710
+ | ------------- | -------- | ----------------------------------------------------------------- |
711
+ | `name` | `string` | POI name or short location name |
712
+ | `address` | `string` | Full formatted address (from Nominatim or reverse geocoding) |
713
+ | `source` | `string` | Data source: `"nominatim"` or custom source ID |
714
+ | `sourceId` | `string` | Original identifier from the source |
715
+ | `sourceLayer` | `string` | Human-readable name of the source layer |
716
+ | `category` | `string` | POI type/category (e.g., `"skating_spot"`, `"bus_stop"`) |
717
+ | `inputMethod` | `string` | How the location was selected: `"search"`, `"poi_click"`, `"map_click"` |
718
+ | `metadata` | `object` | Additional metadata from the source (varies by source) |
719
+
720
+ **Note**: All properties are optional and only included when available. Empty strings and null values are automatically omitted.
721
+
722
+ ### Performance & Visual Design
723
+
724
+ **Zoom-based Visibility**:
725
+
726
+ - POIs hidden when zoomed out (< `poiMinZoom`)
727
+ - Prevents map clutter at country/continent view
728
+ - Markers appear when zooming into city/neighborhood level
729
+
730
+ **Display Limit**:
731
+
732
+ - Maximum `poiMaxDisplay` POIs shown (default: 100)
733
+ - Sorted by distance from map center
734
+ - Only closest/most relevant POIs displayed
735
+
736
+ **Visual Distinction**:
737
+
738
+ - Custom POIs: Configurable colors
739
+ - Selected POI: Orange marker (#ff5200 - Strapi orange)
740
+ - Labels show POI names on hover
741
+
742
+ **Viewport-based Loading**:
743
+
744
+ - POIs query based on current map bounds
745
+ - Updates automatically when panning or zooming
746
+ - Debounced for smooth performance
747
+
748
+ ### Development
749
+
750
+ The plugin uses:
751
+
752
+ - **Build system**: `@strapi/sdk-plugin` for TypeScript compilation
753
+ - **Package manager**: npm
754
+ - **Testing**: Jest for unit tests
755
+ - **Type checking**: TypeScript strict mode
756
+
757
+ Build the plugin:
758
+
759
+ ```bash
760
+ npm run build
761
+ ```
762
+
763
+ Watch mode for development:
764
+
765
+ ```bash
766
+ npm run watch
767
+ ```
768
+
769
+ ## Contributing
770
+
771
+ Bug reports and pull requests are welcome on [Codeberg](https://codeberg.org/premiate-edizioni/strapi-plugin-maplibre-field).
772
+
773
+ ## Credits
774
+
775
+ This plugin was forked from the [Strapi plugin map-field](https://github.com/play14team/strapi-plugin-map-field) by Cédric Pontet and moves from Mapbox to MapLibre with foundations on OpenStreetMap, Nominatim geocoding and Protomaps.
776
+
777
+ Thanks [Enzo Brunii](https://github.com/enzobrunii/strapi-plugin-map-field/commits?author=enzobrunii) for initial hints.
778
+
779
+ ## License
780
+
781
+ [MIT](LICENSE) © Claudio Bernardini / Dipartimento di Cartografia Esistenzialista in Fotta, Premiate Edizioni