@dnax/ui 0.0.48 → 0.0.50
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.
- package/components/QBottomSheet.vue +172 -6
- package/components/QChart.vue +1 -0
- package/components/QDatePicker.vue +1 -1
- package/components/QImagePicker.vue +28 -1
- package/components/QInputChat.vue +859 -0
- package/components/QInputCurrency.vue +319 -0
- package/components/QItemLabel.vue +99 -0
- package/components/QMap.vue +410 -0
- package/components/QNumericKeyboard.vue +304 -0
- package/components/QPage.vue +26 -6
- package/components/QPageContainer.vue +32 -0
- package/components/QPopupProxy.vue +378 -0
- package/components/QQrcode.vue +132 -0
- package/components/QSelect.vue +12 -0
- package/components/QSpreadsheet.vue +395 -69
- package/components/QTime.vue +845 -0
- package/components/QTimeline.vue +111 -0
- package/components/QTimelineEntry.vue +271 -0
- package/index.ts +11 -1
- package/lib/bottomSheet.test.ts +121 -0
- package/lib/bottomSheet.ts +115 -0
- package/lib/color.ts +13 -0
- package/lib/currency.test.ts +193 -0
- package/lib/currency.ts +301 -0
- package/lib/fixedLayout.ts +12 -11
- package/lib/icons.ts +3 -20
- package/lib/map.test.ts +365 -0
- package/lib/map.ts +438 -0
- package/lib/mapEngine.ts +158 -0
- package/lib/mapLeaflet.ts +142 -0
- package/lib/mapMaplibre.ts +106 -0
- package/lib/mapMaptiler.ts +93 -0
- package/lib/numericKeyboard.test.ts +179 -0
- package/lib/numericKeyboard.ts +166 -0
- package/lib/pagePadding.test.ts +37 -0
- package/lib/pagePadding.ts +32 -0
- package/lib/qrcode.test.ts +103 -0
- package/lib/qrcode.ts +125 -0
- package/lib/spreadsheetChanges.test.ts +230 -0
- package/lib/spreadsheetChanges.ts +331 -0
- package/package.json +5 -17
- package/shims.d.ts +19 -0
- package/styles/main.css +689 -884
- package/components/QTiptap.vue +0 -996
- package/components/internal/QTiptapTableView.vue +0 -123
- package/components/internal/QTiptapTaskItemView.vue +0 -49
- package/lib/tiptap-table.ts +0 -19
- package/lib/tiptap-task-list.ts +0 -16
package/lib/map.ts
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// map.ts — `<q-map>` : props → options du SDK JS MapTiler, et conventions des marques.
|
|
2
|
+
//
|
|
3
|
+
// Pur : aucun DOM, aucun import de `@maptiler/sdk` (le composant seul fait le pont, par
|
|
4
|
+
// import dynamique côté client) → tout est testable hors navigateur, comme
|
|
5
|
+
// `lib/chart.ts`. Le SDK est basé sur MapLibre GL JS.
|
|
6
|
+
//
|
|
7
|
+
// <q-map
|
|
8
|
+
// center="[2.35, 48.85]"
|
|
9
|
+
// :zoom="12"
|
|
10
|
+
// :marks="[{ type: 'marker', position: [2.35, 48.85], label: 'Paris' }]"
|
|
11
|
+
// />
|
|
12
|
+
//
|
|
13
|
+
// Référence : https://docs.maptiler.com/sdk-js/ (SDK JS v4)
|
|
14
|
+
|
|
15
|
+
/** Fournisseurs de tuiles supportés.
|
|
16
|
+
* - `maptiler` : SDK JS MapTiler (styles vectoriels, terrain, globe) + clé d'API ;
|
|
17
|
+
* - `maplibre` : **MapLibre GL JS** seul (VectorGL), sans clé — style libre ;
|
|
18
|
+
* - `openstreetmap` : raster OpenStreetMap via le SDK MapTiler, sans clé ;
|
|
19
|
+
* - `leaflet` : moteur Leaflet, raster sans clé (tuiles XYZ quelconques). */
|
|
20
|
+
export type QMapProvider = "maptiler" | "maplibre" | "openstreetmap" | "leaflet"
|
|
21
|
+
|
|
22
|
+
/** Valeurs acceptées par la prop `provider` : les noms canoniques, plus les alias
|
|
23
|
+
* (`osm`, `openstreet`, `map-libre`…) résolus par `providerOf`. */
|
|
24
|
+
export type QMapProviderInput = QMapProvider | "osm" | "openstreet" | "map-libre"
|
|
25
|
+
|
|
26
|
+
export const MAP_PROVIDERS: readonly QMapProvider[] = [
|
|
27
|
+
"maptiler",
|
|
28
|
+
"maplibre",
|
|
29
|
+
"openstreetmap",
|
|
30
|
+
"leaflet",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
/** Alias tolérés → fournisseur canonique. */
|
|
34
|
+
export const PROVIDER_ALIASES: Record<string, QMapProvider> = {
|
|
35
|
+
osm: "openstreetmap",
|
|
36
|
+
openstreet: "openstreetmap",
|
|
37
|
+
"open-street-map": "openstreetmap",
|
|
38
|
+
"map-tiler": "maptiler",
|
|
39
|
+
"map-libre": "maplibre",
|
|
40
|
+
"maplibre-gl": "maplibre",
|
|
41
|
+
"leaflet.js": "leaflet",
|
|
42
|
+
"leaflet-js": "leaflet",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Fournisseur canonique d'une valeur de prop — valeur absente ou inconnue →
|
|
46
|
+
* `maptiler` (le fournisseur par défaut, celui du SDK). */
|
|
47
|
+
export function providerOf(provider?: unknown): QMapProvider {
|
|
48
|
+
if (typeof provider !== "string") return "maptiler"
|
|
49
|
+
const key = provider.trim().toLowerCase()
|
|
50
|
+
if (key === "maptiler" || key === "maplibre" || key === "openstreetmap" || key === "leaflet")
|
|
51
|
+
return key
|
|
52
|
+
return PROVIDER_ALIASES[key] ?? "maptiler"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** La valeur est-elle un fournisseur connu (canonique ou alias) ? `undefined` = défaut,
|
|
56
|
+
* donc connu : sert au composant pour avertir sur une faute de frappe. */
|
|
57
|
+
export function isKnownProvider(provider?: unknown): boolean {
|
|
58
|
+
if (provider === undefined || provider === null || provider === "") return true
|
|
59
|
+
if (typeof provider !== "string") return false
|
|
60
|
+
const key = provider.trim().toLowerCase()
|
|
61
|
+
return (
|
|
62
|
+
key === "maptiler" ||
|
|
63
|
+
key === "maplibre" ||
|
|
64
|
+
key === "openstreetmap" ||
|
|
65
|
+
key === "leaflet" ||
|
|
66
|
+
key in PROVIDER_ALIASES
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Centre par défaut : vue monde. Jamais de géolocalisation IP implicite — le SDK
|
|
71
|
+
* centre la carte sur le visiteur (par IP) quand aucun `center` n'est fourni. */
|
|
72
|
+
export const DEFAULT_CENTER: readonly [number, number] = [0, 20]
|
|
73
|
+
|
|
74
|
+
export const DEFAULT_ZOOM = 1.5
|
|
75
|
+
|
|
76
|
+
/** Hauteur du cadre quand `height` n'est pas fourni (px) */
|
|
77
|
+
export const DEFAULT_MAP_HEIGHT = 320
|
|
78
|
+
|
|
79
|
+
/** Styles nommés acceptés par la prop `style` → clés de `MapStyle` du SDK (« reference
|
|
80
|
+
* styles » du README MapTiler). Toute autre valeur est passée telle quelle au SDK :
|
|
81
|
+
* ID de style (`"outdoor-v2"`) ou URL (`"https://api.maptiler.com/maps/<id>/style.json"`). */
|
|
82
|
+
export const MAP_STYLE_KEYS = [
|
|
83
|
+
"streets",
|
|
84
|
+
"satellite",
|
|
85
|
+
"hybrid",
|
|
86
|
+
"outdoor",
|
|
87
|
+
"winter",
|
|
88
|
+
"dataviz",
|
|
89
|
+
"basic",
|
|
90
|
+
"bright",
|
|
91
|
+
"topo",
|
|
92
|
+
"voyager",
|
|
93
|
+
"toner",
|
|
94
|
+
"ocean",
|
|
95
|
+
"landscape",
|
|
96
|
+
"aquarelle",
|
|
97
|
+
"backdrop",
|
|
98
|
+
"stage",
|
|
99
|
+
"openstreetmap",
|
|
100
|
+
] as const
|
|
101
|
+
|
|
102
|
+
export type QMapStyleKey = (typeof MAP_STYLE_KEYS)[number]
|
|
103
|
+
|
|
104
|
+
/** Clé de `MapStyle` d'un nom court, ou `undefined` si c'est un ID/URL de style. */
|
|
105
|
+
export function mapStyleKey(style: unknown): QMapStyleKey | undefined {
|
|
106
|
+
if (typeof style !== "string") return undefined
|
|
107
|
+
const key = style.trim().toLowerCase()
|
|
108
|
+
return (MAP_STYLE_KEYS as readonly string[]).includes(key) ? (key as QMapStyleKey) : undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Gabarit de tuiles raster par défaut (OpenStreetMap) — utilisé par
|
|
112
|
+
* `provider="openstreetmap"`, `provider="leaflet"` et le repli sans clé MapTiler. */
|
|
113
|
+
export const OSM_TILES = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
114
|
+
|
|
115
|
+
/** Attribution par défaut des tuiles OpenStreetMap (obligatoire à l'affichage). */
|
|
116
|
+
export const OSM_ATTRIBUTION = "© OpenStreetMap contributors"
|
|
117
|
+
|
|
118
|
+
/** Style raster (MapLibre / SDK MapTiler) d'une source de tuiles XYZ. */
|
|
119
|
+
export function osmStyle(tiles: string = OSM_TILES, attribution: string = OSM_ATTRIBUTION) {
|
|
120
|
+
return {
|
|
121
|
+
version: 8,
|
|
122
|
+
sources: {
|
|
123
|
+
osm: { type: "raster", tiles: [tiles], tileSize: 256, maxzoom: 19, attribution },
|
|
124
|
+
},
|
|
125
|
+
layers: [{ id: "osm", type: "raster", source: "osm" }],
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Style raster OpenStreetMap par défaut (`provider="openstreetmap"` / repli sans clé).
|
|
130
|
+
* Aucune clé d'API : c'est ce qui permet à la carte de rester visible en démo. */
|
|
131
|
+
export const OSM_STYLE = osmStyle()
|
|
132
|
+
|
|
133
|
+
/** Coordonnées en convention **Leaflet** : `[lat, lng]` (l'inverse de dnax.ui). */
|
|
134
|
+
export const toLeaflet = (position: readonly [number, number]): [number, number] => [
|
|
135
|
+
position[1],
|
|
136
|
+
position[0],
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
/** Couleur acceptable dans un attribut/motif SVG/HTML (anti-injection dans un `divIcon`).
|
|
140
|
+
* Renvoie `undefined` si la valeur sort du jeu de caractères d'une couleur CSS. */
|
|
141
|
+
export function safeCssColor(value?: unknown): string | undefined {
|
|
142
|
+
if (typeof value !== "string") return undefined
|
|
143
|
+
const color = value.trim()
|
|
144
|
+
return /^[#a-zA-Z0-9(),.%\s\/+-]{1,64}$/.test(color) ? color : undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Longitude/latitude — `[lng, lat]`, `"lng,lat"` (ou `"[lng, lat]"`) ou `{ lng, lat }` */
|
|
148
|
+
export type QMapPosition =
|
|
149
|
+
| [number, number]
|
|
150
|
+
| string
|
|
151
|
+
| { lng?: number | string; lat?: number | string; lon?: number | string }
|
|
152
|
+
|
|
153
|
+
const toNumber = (value: unknown): number | undefined => {
|
|
154
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : undefined
|
|
155
|
+
if (typeof value !== "string" || value.trim() === "") return undefined
|
|
156
|
+
const n = Number(value)
|
|
157
|
+
return Number.isFinite(n) ? n : undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Coordonnées valides (lng ∈ [-180, 180], lat ∈ [-90, 90]) — `[lng, lat]`. */
|
|
161
|
+
const coordinates = (lng: unknown, lat: unknown): [number, number] | undefined => {
|
|
162
|
+
const x = toNumber(lng)
|
|
163
|
+
const y = toNumber(lat)
|
|
164
|
+
if (x === undefined || y === undefined) return undefined
|
|
165
|
+
if (x < -180 || x > 180 || y < -90 || y > 90) return undefined
|
|
166
|
+
return [x, y]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Position `[lng, lat]`, ou `undefined` si la valeur est absente/illisible/hors bornes. */
|
|
170
|
+
export function parsePosition(value?: unknown): [number, number] | undefined {
|
|
171
|
+
if (Array.isArray(value)) return coordinates(value[0], value[1])
|
|
172
|
+
|
|
173
|
+
if (typeof value === "string") {
|
|
174
|
+
// "[2.35, 48.85]", "2.35, 48.85" ou "2.35 48.85"
|
|
175
|
+
const parts = value.replace(/[[\]()]/g, "").split(/[,;\s]+/).filter(Boolean)
|
|
176
|
+
return parts.length >= 2 ? coordinates(parts[0], parts[1]) : undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (value && typeof value === "object") {
|
|
180
|
+
const o = value as Record<string, unknown>
|
|
181
|
+
return coordinates(o.lng ?? o.lon, o.lat)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Zoom borné à [0, 24]. */
|
|
188
|
+
export function parseZoom(value: unknown): number | undefined {
|
|
189
|
+
const n = toNumber(value)
|
|
190
|
+
return n === undefined ? undefined : Math.min(24, Math.max(0, n))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Hauteur CSS du cadre : nombre → px, chaîne CSS telle quelle, sinon défaut. */
|
|
194
|
+
export function mapHeight(value: unknown): string {
|
|
195
|
+
const n = toNumber(value)
|
|
196
|
+
if (n !== undefined && n > 0) return `${n}px`
|
|
197
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : `${DEFAULT_MAP_HEIGHT}px`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── Marques ────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export type QMapMarkType = "marker" | "popup"
|
|
203
|
+
|
|
204
|
+
/** Une marque est un **objet littéral plat** (comme les marks de `<q-chart>`) :
|
|
205
|
+
* `marker` = épingle (+ bulle optionnelle), `popup` = bulle seule. */
|
|
206
|
+
export interface QMapMark {
|
|
207
|
+
/** Type de marque */
|
|
208
|
+
type: QMapMarkType
|
|
209
|
+
/** Position `[lng, lat]` (ou `"lng,lat"`, ou `{ lng, lat }`) */
|
|
210
|
+
position?: QMapPosition
|
|
211
|
+
/** Longitude — alternative à `position` */
|
|
212
|
+
lng?: number | string
|
|
213
|
+
/** Latitude — alternative à `position` */
|
|
214
|
+
lat?: number | string
|
|
215
|
+
/** Texte de la bulle (échappé) — ou libellé du marqueur */
|
|
216
|
+
label?: string
|
|
217
|
+
/** Contenu HTML de la bulle (prioritaire sur `label`, inséré tel quel) */
|
|
218
|
+
html?: string
|
|
219
|
+
/** Couleur du marqueur : token dnax (`primary`, `chart-2`…) ou couleur CSS */
|
|
220
|
+
color?: string
|
|
221
|
+
/** Bulle ouverte au chargement (les `popup` sont ouvertes par défaut) */
|
|
222
|
+
open?: boolean
|
|
223
|
+
/** Décalage de la bulle en px (défaut 18) */
|
|
224
|
+
offset?: number
|
|
225
|
+
/** Épingle déplaçable à la souris */
|
|
226
|
+
draggable?: boolean
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Charge utile de `@pick` : la marque cliquée et sa position. */
|
|
230
|
+
export interface QMapPick {
|
|
231
|
+
mark: QMapMark
|
|
232
|
+
index: number
|
|
233
|
+
position: [number, number]
|
|
234
|
+
lng: number
|
|
235
|
+
lat: number
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Position d'une marque (`position`, sinon `lng`/`lat`). */
|
|
239
|
+
export function markPosition(mark: QMapMark): [number, number] | undefined {
|
|
240
|
+
return parsePosition(mark.position) ?? coordinates(mark.lng, mark.lat)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ESCAPES: Record<string, string> = {
|
|
244
|
+
"&": "&",
|
|
245
|
+
"<": "<",
|
|
246
|
+
">": ">",
|
|
247
|
+
'"': """,
|
|
248
|
+
"'": "'",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Échappe un texte destiné à `innerHTML` (`label` d'une marque). */
|
|
252
|
+
export const escapeHtml = (text: string): string =>
|
|
253
|
+
text.replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char)
|
|
254
|
+
|
|
255
|
+
/** Contenu de la bulle : `html` s'il est fourni, sinon `label` échappé, sinon rien. */
|
|
256
|
+
export function popupHtml(mark: QMapMark): string | undefined {
|
|
257
|
+
if (typeof mark.html === "string" && mark.html.trim() !== "") return mark.html
|
|
258
|
+
if (typeof mark.label === "string" && mark.label.trim() !== "") return escapeHtml(mark.label)
|
|
259
|
+
return undefined
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface QMapMarkSpec {
|
|
263
|
+
mark: QMapMark
|
|
264
|
+
index: number
|
|
265
|
+
position: [number, number]
|
|
266
|
+
html?: string
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Marques exploitables, dans l'ordre — une marque sans position valide est ignorée. */
|
|
270
|
+
export function marksOf(marks?: readonly QMapMark[] | null): QMapMarkSpec[] {
|
|
271
|
+
const out: QMapMarkSpec[] = []
|
|
272
|
+
if (!Array.isArray(marks)) return out
|
|
273
|
+
|
|
274
|
+
marks.forEach((mark, index) => {
|
|
275
|
+
if (!mark || typeof mark !== "object") return
|
|
276
|
+
const position = markPosition(mark)
|
|
277
|
+
if (!position) return
|
|
278
|
+
out.push({ mark, index, position, html: popupHtml(mark) })
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
return out
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── Options du constructeur `Map` ──────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
export interface QMapOptionsInput {
|
|
287
|
+
provider?: QMapProviderInput
|
|
288
|
+
apiKey?: string
|
|
289
|
+
/** Style **déjà résolu** (MapStyle du SDK, variante, ID, URL ou objet de style) */
|
|
290
|
+
style?: unknown
|
|
291
|
+
/** Gabarit de tuiles XYZ pour les fournisseurs raster (`openstreetmap`, `leaflet`) */
|
|
292
|
+
tiles?: string
|
|
293
|
+
/** Attribution des tuiles (obligatoire pour OpenStreetMap) */
|
|
294
|
+
attribution?: string
|
|
295
|
+
center?: unknown
|
|
296
|
+
zoom?: unknown
|
|
297
|
+
/** Contrôles du SDK : `true` (position par défaut), `false`, ou un coin
|
|
298
|
+
* (`top-left`, `top-right`, `bottom-left`, `bottom-right`) */
|
|
299
|
+
navigation?: boolean | string
|
|
300
|
+
geolocate?: boolean | string
|
|
301
|
+
scale?: boolean | string
|
|
302
|
+
fullscreen?: boolean | string
|
|
303
|
+
/** Options brutes du SDK — fusionnées en **dernier** */
|
|
304
|
+
options?: Record<string, any>
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Options passées à `new Map(...)` du SDK. `provider="openstreetmap"` impose le style
|
|
309
|
+
* raster OpenStreetMap (aucune clé) ; tout le reste est délégué au SDK, et `options` est
|
|
310
|
+
* fusionné en dernier (règle d'or : toute option brute passe par `options`).
|
|
311
|
+
*
|
|
312
|
+
* `terrain` et `projection` **ne sont pas** ici : le SDK les applique au constructeur
|
|
313
|
+
* avant que le style soit chargé et jette alors « Style is not done loading » (vérifié
|
|
314
|
+
* au navigateur) — `<q-map>` les applique après le `load`, via `enableTerrain()` et
|
|
315
|
+
* `setProjection()`.
|
|
316
|
+
*/
|
|
317
|
+
export function mapOptionsOf(input: QMapOptionsInput = {}): Record<string, any> {
|
|
318
|
+
const provider: QMapProvider = providerOf(input.provider)
|
|
319
|
+
|
|
320
|
+
const out: Record<string, any> = {
|
|
321
|
+
center: parsePosition(input.center) ?? DEFAULT_CENTER,
|
|
322
|
+
zoom: parseZoom(input.zoom) ?? DEFAULT_ZOOM,
|
|
323
|
+
style:
|
|
324
|
+
provider === "openstreetmap"
|
|
325
|
+
? osmStyle(input.tiles ?? OSM_TILES, input.attribution ?? OSM_ATTRIBUTION)
|
|
326
|
+
: input.style,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (provider === "maptiler" && input.apiKey) out.apiKey = input.apiKey
|
|
330
|
+
if (input.navigation !== undefined) out.navigationControl = input.navigation
|
|
331
|
+
if (input.geolocate !== undefined) out.geolocateControl = input.geolocate
|
|
332
|
+
if (input.scale !== undefined) out.scaleControl = input.scale
|
|
333
|
+
if (input.fullscreen !== undefined) out.fullscreenControl = input.fullscreen
|
|
334
|
+
|
|
335
|
+
return { ...out, ...input.options }
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── Options du moteur Leaflet ──────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
export interface QMapLeafletOptions {
|
|
341
|
+
/** Centre en convention Leaflet : `[lat, lng]` */
|
|
342
|
+
center: [number, number]
|
|
343
|
+
zoom: number
|
|
344
|
+
/** Gabarit de tuiles XYZ (`{z}`/`{x}`/`{y}`) */
|
|
345
|
+
tiles: string
|
|
346
|
+
/** Attribution des tuiles */
|
|
347
|
+
attribution: string
|
|
348
|
+
/** Options passées à `L.map()` (les `options` utilisateur, `tileLayer` exclu) */
|
|
349
|
+
mapOptions: Record<string, any>
|
|
350
|
+
/** Options passées à `L.tileLayer()` (`options.tileLayer`) */
|
|
351
|
+
layerOptions: Record<string, any>
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Options du moteur **Leaflet** : la couche de tuiles et les options de `L.map()`.
|
|
356
|
+
*
|
|
357
|
+
* Convention de coordonnées : **`[lat, lng]`** — Leaflet l'exige, alors que dnax.ui
|
|
358
|
+
* expose `[lng, lat]` partout ailleurs (la conversion est faite ici, une fois).
|
|
359
|
+
* `options` est passé à `L.map()`, sa clé `tileLayer` à `L.tileLayer()`.
|
|
360
|
+
*/
|
|
361
|
+
export function leafletOptionsOf(input: QMapOptionsInput = {}): QMapLeafletOptions {
|
|
362
|
+
const { tileLayer, ...mapOptions } = (input.options ?? {}) as Record<string, any>
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
center: toLeaflet(parsePosition(input.center) ?? DEFAULT_CENTER),
|
|
366
|
+
zoom: parseZoom(input.zoom) ?? DEFAULT_ZOOM,
|
|
367
|
+
tiles: typeof input.tiles === "string" && input.tiles.trim() ? input.tiles.trim() : OSM_TILES,
|
|
368
|
+
attribution:
|
|
369
|
+
typeof input.attribution === "string" && input.attribution.trim()
|
|
370
|
+
? input.attribution.trim()
|
|
371
|
+
: OSM_ATTRIBUTION,
|
|
372
|
+
mapOptions,
|
|
373
|
+
layerOptions: (tileLayer as Record<string, any>) ?? {},
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── MapLibre GL JS (styles libres, sans clé) ───────────────────────────────────
|
|
378
|
+
|
|
379
|
+
/** Style de démonstration MapLibre (monde entier, aucune clé) — défaut du fournisseur. */
|
|
380
|
+
export const MAPLIBRE_DEMO_STYLE = "https://demotiles.maplibre.org/style.json"
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Style d'un `<q-map provider="maplibre">` : `demotiles` (défaut), `openstreetmap` (le
|
|
384
|
+
* raster de `osmStyle()`, `tiles`/`attribution` compris), une URL de style, un objet de
|
|
385
|
+
* style complet — ou `streets`, la valeur par défaut du design system, qui n'a pas de
|
|
386
|
+
* sens sans MapTiler et retombe donc sur le style de démo.
|
|
387
|
+
*
|
|
388
|
+
* `known` vaut `false` pour un nom non reconnu : le moteur le signale en console
|
|
389
|
+
* (sinon un `map-style="topo"` afficherait le style de démo en silence).
|
|
390
|
+
*/
|
|
391
|
+
export function maplibreStyle(input: QMapOptionsInput = {}): { style: any; known: boolean } {
|
|
392
|
+
const value = input.style
|
|
393
|
+
if (value && typeof value === "object") return { style: value, known: true }
|
|
394
|
+
|
|
395
|
+
const raw = typeof value === "string" ? value.trim() : ""
|
|
396
|
+
const key = raw.toLowerCase()
|
|
397
|
+
|
|
398
|
+
if (key === "" || key === "streets" || key === "demotiles" || key === "demo")
|
|
399
|
+
return { style: MAPLIBRE_DEMO_STYLE, known: true }
|
|
400
|
+
|
|
401
|
+
if (key === "openstreetmap" || key === "osm")
|
|
402
|
+
return {
|
|
403
|
+
style: osmStyle(input.tiles ?? OSM_TILES, input.attribution ?? OSM_ATTRIBUTION),
|
|
404
|
+
known: true,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// URL absolue ou relative (le reste est un nom court inconnu)
|
|
408
|
+
if (/^(https?:)?\/\//.test(raw) || raw.startsWith("/") || raw.startsWith("."))
|
|
409
|
+
return { style: raw, known: true }
|
|
410
|
+
|
|
411
|
+
return { style: MAPLIBRE_DEMO_STYLE, known: false }
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export interface QMapMaplibreOptions {
|
|
415
|
+
/** Options passées à `new Map()` de MapLibre GL JS */
|
|
416
|
+
map: Record<string, any>
|
|
417
|
+
/** Le `map-style` demandé a-t-il été reconnu ? (sinon repli sur le style de démo) */
|
|
418
|
+
styleKnown: boolean
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Options du moteur **MapLibre GL JS** : `center` / `zoom` et le style résolu
|
|
423
|
+
* (`maplibreStyle`). MapLibre n'a ni clé d'API, ni catalogue de styles : c'est le style
|
|
424
|
+
* qui porte les sources de données.
|
|
425
|
+
*/
|
|
426
|
+
export function maplibreOptionsOf(input: QMapOptionsInput = {}): QMapMaplibreOptions {
|
|
427
|
+
const { style, known } = maplibreStyle(input)
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
map: {
|
|
431
|
+
center: parsePosition(input.center) ?? DEFAULT_CENTER,
|
|
432
|
+
zoom: parseZoom(input.zoom) ?? DEFAULT_ZOOM,
|
|
433
|
+
style,
|
|
434
|
+
...(input.options ?? {}),
|
|
435
|
+
},
|
|
436
|
+
styleKnown: known,
|
|
437
|
+
}
|
|
438
|
+
}
|
package/lib/mapEngine.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// mapEngine — contrat commun aux moteurs de carte de `<q-map>`.
|
|
2
|
+
//
|
|
3
|
+
// `<q-map>` ne connaît que ce contrat : la prop `provider` choisit un moteur, qui traduit
|
|
4
|
+
// centre / zoom / marques / style vers sa bibliothèque (SDK MapTiler, Leaflet) et convertit
|
|
5
|
+
// les conventions de coordonnées (`[lng, lat]` côté dnax.ui). Tout ce qui ne dépend pas du
|
|
6
|
+
// moteur — props, thème, surcouche de chargement, réactivité — reste dans le composant.
|
|
7
|
+
//
|
|
8
|
+
// Les deux moteurs sont importés **à la demande** (`createMapEngine`) : un `<q-map
|
|
9
|
+
// provider="leaflet">` ne charge jamais le SDK MapTiler, `provider="maplibre"` ne charge
|
|
10
|
+
// jamais Leaflet, et inversement.
|
|
11
|
+
import type { QMapMarkSpec, QMapPick, QMapProvider } from "./map"
|
|
12
|
+
|
|
13
|
+
/** Moteur de carte : surface minimale dont `<q-map>` a besoin. */
|
|
14
|
+
export interface MapEngine {
|
|
15
|
+
/** Instance native (`Map` du SDK MapTiler, `L.Map` de Leaflet) */
|
|
16
|
+
native: any
|
|
17
|
+
/** Centre `[lng, lat]` + zoom */
|
|
18
|
+
setView(center: [number, number], zoom: number): void
|
|
19
|
+
/** Style de carte : nom court (`streets`, `dataviz`…), ID/URL, ou objet de style —
|
|
20
|
+
* `dark` demande la variante sombre quand le moteur en a une. Sans effet sur les
|
|
21
|
+
* moteurs raster (Leaflet). */
|
|
22
|
+
setStyle(style: unknown, dark?: boolean): void
|
|
23
|
+
/** Couche de tuiles raster — sans effet sur MapTiler, qui passe par `setStyle` */
|
|
24
|
+
setRaster?(tiles: string, attribution?: string): void
|
|
25
|
+
/** Redessine les marques (marqueurs, épingles et bulles) */
|
|
26
|
+
setMarks(marks: QMapMarkSpec[]): void
|
|
27
|
+
/** Relief 3D — sans effet si le moteur ne le gère pas */
|
|
28
|
+
setTerrain(on: boolean): void
|
|
29
|
+
/** Projection — sans effet si le moteur ne la gère pas */
|
|
30
|
+
setProjection(projection: string): void
|
|
31
|
+
/** Recalcule la taille du canvas après un changement de conteneur */
|
|
32
|
+
resize(): void
|
|
33
|
+
/** Détruit la carte et ses marques */
|
|
34
|
+
destroy(): void
|
|
35
|
+
/** Carte utilisable — le callback part tout de suite si elle l'est déjà */
|
|
36
|
+
onReady(callback: (engine: MapEngine) => void): void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Contexte transmis à un moteur par `<q-map>`. */
|
|
40
|
+
export interface MapEngineInput {
|
|
41
|
+
/** Fournisseur retenu (le composant a déjà normalisé la valeur) */
|
|
42
|
+
provider: QMapProvider
|
|
43
|
+
/** Élément conteneur : le moteur y monte sa carte */
|
|
44
|
+
el: HTMLElement
|
|
45
|
+
/** Options **déjà construites** pour ce moteur (`mapOptionsOf` / `leafletOptionsOf`) */
|
|
46
|
+
options: Record<string, any>
|
|
47
|
+
/** Centre `[lng, lat]` de départ */
|
|
48
|
+
center: [number, number]
|
|
49
|
+
/** Zoom de départ */
|
|
50
|
+
zoom: number
|
|
51
|
+
/** Mode sombre courant (variante `DARK` des styles MapTiler) */
|
|
52
|
+
dark: boolean
|
|
53
|
+
/** Marques de départ (normalisées par `marksOf`) */
|
|
54
|
+
marks: QMapMarkSpec[]
|
|
55
|
+
/** Contrôles demandés (props `navigation`, `geolocate`, `scale`, `fullscreen`) : le SDK
|
|
56
|
+
* MapTiler les reçoit par ses options, les autres moteurs les ajoutent eux-mêmes */
|
|
57
|
+
controls?: {
|
|
58
|
+
navigation?: boolean | string
|
|
59
|
+
geolocate?: boolean | string
|
|
60
|
+
scale?: boolean | string
|
|
61
|
+
fullscreen?: boolean | string
|
|
62
|
+
}
|
|
63
|
+
/** Couleur concrète d'une marque (token du thème → couleur CSS) */
|
|
64
|
+
color: (value?: string) => string | undefined
|
|
65
|
+
/** Une marque a été cliquée */
|
|
66
|
+
pick: (payload: QMapPick) => void
|
|
67
|
+
/** Erreur du moteur — y compris non fatale (tuile manquante…) */
|
|
68
|
+
error: (error: unknown) => void
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Fabrique de moteur : `maptiler` / `openstreetmap` → SDK MapTiler, `maplibre` →
|
|
72
|
+
* MapLibre GL JS seul, `leaflet` → Leaflet. */
|
|
73
|
+
export async function createMapEngine(input: MapEngineInput): Promise<MapEngine> {
|
|
74
|
+
if (input.provider === "leaflet") {
|
|
75
|
+
const { createLeafletEngine } = await import("./mapLeaflet")
|
|
76
|
+
return createLeafletEngine(input)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (input.provider === "maplibre") {
|
|
80
|
+
const { createMaplibreEngine } = await import("./mapMaplibre")
|
|
81
|
+
return createMaplibreEngine(input)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { createMaptilerEngine } = await import("./mapMaptiler")
|
|
85
|
+
return createMaptilerEngine(input)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Positions de contrôle communes à MapLibre et à Leaflet. */
|
|
89
|
+
export const CONTROL_POSITIONS = ["top-left", "top-right", "bottom-left", "bottom-right"] as const
|
|
90
|
+
|
|
91
|
+
/** Position de contrôle à partir d'une prop `boolean | string` (`true` → la position par
|
|
92
|
+
* défaut du moteur, une chaîne valide → elle-même). */
|
|
93
|
+
export function controlPosition(
|
|
94
|
+
value: boolean | string | undefined,
|
|
95
|
+
fallback: (typeof CONTROL_POSITIONS)[number],
|
|
96
|
+
): (typeof CONTROL_POSITIONS)[number] {
|
|
97
|
+
return typeof value === "string" && (CONTROL_POSITIONS as readonly string[]).includes(value)
|
|
98
|
+
? (value as (typeof CONTROL_POSITIONS)[number])
|
|
99
|
+
: fallback
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Construit les marques DOM d'un moteur **MapLibre** — le SDK MapTiler et `maplibre-gl`
|
|
104
|
+
* exposent les mêmes `Marker` / `Popup` : ce code est partagé par les deux moteurs.
|
|
105
|
+
* Renvoie la fonction de démontage (à appeler avant d'en construire d'autres).
|
|
106
|
+
*/
|
|
107
|
+
export function buildMapLibreMarks(
|
|
108
|
+
api: { Marker: any; Popup: any },
|
|
109
|
+
map: any,
|
|
110
|
+
marks: QMapMarkSpec[],
|
|
111
|
+
input: MapEngineInput,
|
|
112
|
+
): () => void {
|
|
113
|
+
const created: Array<() => void> = []
|
|
114
|
+
|
|
115
|
+
for (const { mark, index, position, html } of marks) {
|
|
116
|
+
const onPick = () => input.pick({ mark, index, position, lng: position[0], lat: position[1] })
|
|
117
|
+
const offset = typeof mark.offset === "number" ? mark.offset : 18
|
|
118
|
+
|
|
119
|
+
if (mark.type === "popup") {
|
|
120
|
+
if (!html) continue // une bulle sans contenu n'a rien à montrer
|
|
121
|
+
const popup = new api.Popup({ offset, className: "q-map__popup", closeOnClick: true })
|
|
122
|
+
.setLngLat(position)
|
|
123
|
+
.setHTML(html)
|
|
124
|
+
if (mark.open !== false) popup.addTo(map)
|
|
125
|
+
|
|
126
|
+
const node = popup.getElement() as HTMLElement | undefined
|
|
127
|
+
node?.addEventListener("click", onPick)
|
|
128
|
+
created.push(() => {
|
|
129
|
+
node?.removeEventListener("click", onPick)
|
|
130
|
+
popup.remove()
|
|
131
|
+
})
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// marker
|
|
136
|
+
const markerOptions: Record<string, any> = {}
|
|
137
|
+
const color = input.color(mark.color)
|
|
138
|
+
if (color) markerOptions.color = color
|
|
139
|
+
if (mark.draggable) markerOptions.draggable = true
|
|
140
|
+
|
|
141
|
+
const marker = new api.Marker(markerOptions).setLngLat(position)
|
|
142
|
+
if (html) marker.setPopup(new api.Popup({ offset, className: "q-map__popup" }).setHTML(html))
|
|
143
|
+
marker.addTo(map)
|
|
144
|
+
|
|
145
|
+
const node = marker.getElement() as HTMLElement | undefined
|
|
146
|
+
node?.addEventListener("click", onPick)
|
|
147
|
+
if (mark.open && html) marker.togglePopup()
|
|
148
|
+
|
|
149
|
+
created.push(() => {
|
|
150
|
+
node?.removeEventListener("click", onPick)
|
|
151
|
+
marker.remove()
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return () => {
|
|
156
|
+
for (const dispose of created) dispose()
|
|
157
|
+
}
|
|
158
|
+
}
|