@geosdi/ngx-geocoding 1.1.4 → 1.2.1
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/README.md +200 -34
- package/fesm2022/geosdi-ngx-geocoding.mjs +527 -19
- package/fesm2022/geosdi-ngx-geocoding.mjs.map +1 -1
- package/package.json +4 -3
- package/types/geosdi-ngx-geocoding.d.ts +281 -0
- package/index.d.ts +0 -44
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { NgModule, inject, Injectable } from '@angular/core';
|
|
2
|
+
import { NgModule, InjectionToken, inject, Injectable } from '@angular/core';
|
|
3
3
|
import { HttpClient } from '@angular/common/http';
|
|
4
|
-
import { Subject } from 'rxjs';
|
|
5
|
-
import {
|
|
4
|
+
import { Subject, of, from, throwError, Observable } from 'rxjs';
|
|
5
|
+
import { catchError, switchMap, map, tap } from 'rxjs/operators';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @author Vito Salvia - CNR IMAA geoSDI Group
|
|
9
9
|
* @email vito.salvia@gmail.com
|
|
10
10
|
*/
|
|
11
11
|
class NgxGeocodingModule {
|
|
12
|
-
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
13
|
-
/** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "
|
|
14
|
-
/** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "
|
|
12
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgxGeocodingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
13
|
+
/** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.9", ngImport: i0, type: NgxGeocodingModule }); }
|
|
14
|
+
/** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgxGeocodingModule }); }
|
|
15
15
|
}
|
|
16
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
16
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgxGeocodingModule, decorators: [{
|
|
17
17
|
type: NgModule,
|
|
18
18
|
args: [{
|
|
19
19
|
declarations: [],
|
|
@@ -26,27 +26,329 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.6", ngImpor
|
|
|
26
26
|
* @author Vito Salvia - CNR IMAA geoSDI Group
|
|
27
27
|
* @email vito.salvia@gmail.com
|
|
28
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* La configurazione della ricerca, che **può non esserci** : in quel caso vale
|
|
31
|
+
* il comportamento storico, cioè Nominatim e basta.
|
|
32
|
+
*/
|
|
33
|
+
const NGX_GEOCODING_CONFIG = new InjectionToken("NGX_GEOCODING_CONFIG");
|
|
34
|
+
/**
|
|
35
|
+
* Dichiara da dove arrivano gli indirizzi, nei `providers` dell'applicazione :
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* provideNgxGeocoding({
|
|
39
|
+
* sources: ["google", "nominatim"],
|
|
40
|
+
* country: "it",
|
|
41
|
+
* bias: { lat: 40.83, lon: 14.11, radiusM: 25_000 },
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* L'ordine è quello in cui le sorgenti si provano, e una che non è utilizzabile
|
|
46
|
+
* si salta : la stessa riga vale quindi per il deploy che ha la chiave Google e
|
|
47
|
+
* per quello che non ce l'ha.
|
|
48
|
+
*
|
|
49
|
+
* Chi non la chiama non cambia niente.
|
|
50
|
+
*/
|
|
51
|
+
function provideNgxGeocoding(config) {
|
|
52
|
+
return { provide: NGX_GEOCODING_CONFIG, useValue: config };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* L'SDK Maps JS dell'applicazione ospite, e l'attesa perché sia pronto.
|
|
57
|
+
*
|
|
58
|
+
* **Questa libreria non lo carica** : il tag lo mette in pagina chi la usa, con
|
|
59
|
+
* la propria chiave —
|
|
60
|
+
*
|
|
61
|
+
* ```html
|
|
62
|
+
* <script defer src="https://maps.googleapis.com/maps/api/js?key=…&libraries=places&language=it"></script>
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* — e le ragioni perché resti così sono due : la chiave è dell'applicazione e
|
|
66
|
+
* non deve passare di qui, e caricarlo una seconda volta vorrebbe dire due
|
|
67
|
+
* copie dell'SDK nella stessa pagina.
|
|
68
|
+
*
|
|
69
|
+
* Serve però **aspettarlo** : quel tag è di norma `defer`, quindi chi cerca un
|
|
70
|
+
* indirizzo subito dopo l'avvio può arrivare prima di lui.
|
|
71
|
+
*
|
|
72
|
+
* Della query contano due parametri : `libraries=places`, senza cui non c'è
|
|
73
|
+
* niente da aspettare, e `language`, che decide la lingua delle proposte —
|
|
74
|
+
* `getPlacePredictions` non ha un parametro per chiederla.
|
|
75
|
+
*
|
|
76
|
+
* @author Vito Salvia - CNR IMAA geoSDI Group
|
|
77
|
+
* @email vito.salvia@gmail.com
|
|
78
|
+
*/
|
|
79
|
+
/** Quanto si aspetta l'SDK prima di rinunciare, e ogni quanto lo si guarda. */
|
|
80
|
+
const ATTESA = {
|
|
81
|
+
tetto_ms: 10_000,
|
|
82
|
+
passo_ms: 100,
|
|
83
|
+
};
|
|
84
|
+
/** Gli stati dell'SDK su cui si decide qualcosa. */
|
|
85
|
+
const STATO_PLACES = {
|
|
86
|
+
ok: "OK",
|
|
87
|
+
nessuno: "ZERO_RESULTS",
|
|
88
|
+
negato: "REQUEST_DENIED",
|
|
89
|
+
quota: "OVER_QUERY_LIMIT",
|
|
90
|
+
};
|
|
91
|
+
/** La libreria `places` se è già in pagina, `undefined` altrimenti. */
|
|
92
|
+
function places() {
|
|
93
|
+
return window.google?.maps?.places;
|
|
94
|
+
}
|
|
95
|
+
/** Il costruttore del `Geocoder`, che sta nell'SDK di base. */
|
|
96
|
+
function geocoder() {
|
|
97
|
+
return window.google?.maps?.Geocoder;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* La libreria `places`, aspettando che l'SDK finisca di caricarsi.
|
|
101
|
+
*
|
|
102
|
+
* Rifiuta dopo {@link ATTESA} : il chiamante ripiega sulla strada successiva,
|
|
103
|
+
* perché una ricerca che non risponde mai è peggio di una che risponde meno
|
|
104
|
+
* bene. Il motivo tipico di un'attesa che scade è che al tag dello script manchi
|
|
105
|
+
* `libraries=places` — l'SDK c'è, ma quella libreria no.
|
|
106
|
+
*/
|
|
107
|
+
function attendiPlaces() {
|
|
108
|
+
return attendi(places, "la libreria 'places' (manca `libraries=places` ?)");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Il `Geocoder`, aspettando l'SDK.
|
|
112
|
+
*
|
|
113
|
+
* **Non vuole `libraries=places`** : sta nell'SDK di base, ed è la ragione per
|
|
114
|
+
* cui regge da ultima spiaggia anche dove Places non è disponibile.
|
|
115
|
+
*/
|
|
116
|
+
function attendiGeocoder() {
|
|
117
|
+
return attendi(geocoder, "il Geocoder").then((costruttore) => new costruttore());
|
|
118
|
+
}
|
|
119
|
+
/** L'attesa vera, uguale per le due parti dell'SDK. */
|
|
120
|
+
function attendi(cerca, cosa) {
|
|
121
|
+
const gia = cerca();
|
|
122
|
+
if (gia)
|
|
123
|
+
return Promise.resolve(gia);
|
|
124
|
+
return new Promise((risolvi, rifiuta) => {
|
|
125
|
+
const scadenza = Date.now() + ATTESA.tetto_ms;
|
|
126
|
+
const attesa = setInterval(() => {
|
|
127
|
+
const pronto = cerca();
|
|
128
|
+
if (pronto) {
|
|
129
|
+
clearInterval(attesa);
|
|
130
|
+
risolvi(pronto);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (Date.now() > scadenza) {
|
|
134
|
+
clearInterval(attesa);
|
|
135
|
+
rifiuta(new Error(`SDK Maps senza ${cosa}`));
|
|
136
|
+
}
|
|
137
|
+
}, ATTESA.passo_ms);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @author Vito Salvia - CNR IMAA geoSDI Group
|
|
143
|
+
* @email vito.salvia@gmail.com
|
|
144
|
+
*/
|
|
145
|
+
/** Il servizio pubblico di OpenStreetMap, quando non se ne dichiara un altro. */
|
|
146
|
+
const NOMINATIM = "https://nominatim.openstreetmap.org/search.php";
|
|
147
|
+
/** Le sorgenti da provare quando l'applicazione non dice niente. */
|
|
148
|
+
const SOLO_NOMINATIM = ["nominatim"];
|
|
149
|
+
/** Quanti metri fa un grado di latitudine, per il riquadro del Geocoder. */
|
|
150
|
+
const METRI_PER_GRADO = 111_320;
|
|
151
|
+
/**
|
|
152
|
+
* La ricerca di indirizzi, da una o più sorgenti.
|
|
153
|
+
*
|
|
154
|
+
* **Senza configurazione fa quello che ha sempre fatto** : interroga Nominatim,
|
|
155
|
+
* che non vuole nessuna chiave. Google si accende con
|
|
156
|
+
* `provideNgxGeocoding({ sources: ["google", "nominatim"] })`, e vuole che
|
|
157
|
+
* l'applicazione abbia in pagina l'**SDK Maps** con `libraries=places`.
|
|
158
|
+
*
|
|
159
|
+
* **Perché l'SDK e non una chiamata a Google**, che è la cosa da sapere prima
|
|
160
|
+
* di «semplificare» questo codice : delle API di Google dal browser si può
|
|
161
|
+
* chiamare la sola **Places (New)**, l'unica che esponga CORS, e quel prodotto
|
|
162
|
+
* va abilitato a parte sul progetto Cloud — dove non lo è, una `fetch` prende
|
|
163
|
+
* `403 SERVICE_DISABLED`. **Places legacy** e **Geocoding** CORS non ce l'hanno
|
|
164
|
+
* affatto, ma l'SDK le raggiunge da sé : passando da lui la ricerca funziona
|
|
165
|
+
* con le abilitazioni che il progetto ha già.
|
|
166
|
+
*
|
|
167
|
+
* Dentro `google` c'è quindi una **cascata**, ed è la stessa che si scriverebbe
|
|
168
|
+
* lato server :
|
|
169
|
+
*
|
|
170
|
+
* 1. **`AutocompleteSuggestion`** (Places **New**), che è la strada
|
|
171
|
+
* raccomandata da Google dal 1° marzo 2025 e l'unica offerta ai progetti
|
|
172
|
+
* nuovi. Sotto sta un prodotto che va **abilitato a parte** sulla console
|
|
173
|
+
* Cloud : dove non lo è risponde `403`, e si scende;
|
|
174
|
+
* 2. **`AutocompleteService`** (Places legacy), che dà le stesse proposte
|
|
175
|
+
* incrementali. Google la dà per deprecata — «non disponibile per i nuovi
|
|
176
|
+
* clienti», con dodici mesi di preavviso prima di spegnerla — ma è quella
|
|
177
|
+
* che risponde sui progetti dove la New non è stata abilitata;
|
|
178
|
+
* 3. **`Geocoder`**, quando nemmeno Places risponde : nessuna proposta
|
|
179
|
+
* incrementale, di norma un risultato solo, ma **porta già le coordinate** e
|
|
180
|
+
* sta nell'SDK di base, quindi non dipende da `libraries=places`.
|
|
181
|
+
*
|
|
182
|
+
* I gradini si scendono da sé e non si risalgono nella stessa sessione. Il
|
|
183
|
+
* primo è in cima **anche se oggi, su un progetto senza la New, fallisce
|
|
184
|
+
* sempre** : costa una richiesta negata per sessione, e il giorno in cui quel
|
|
185
|
+
* prodotto viene abilitato la ricerca si sposta sulla strada giusta senza che
|
|
186
|
+
* nessuno tocchi il codice.
|
|
187
|
+
*
|
|
188
|
+
* **Le proposte di Places non portano le coordinate**, che si chiedono per il
|
|
189
|
+
* solo luogo scelto : vedi {@link resolve}, ed è il motivo per cui esiste.
|
|
190
|
+
*/
|
|
29
191
|
class NgxGeocodingService {
|
|
30
192
|
constructor() {
|
|
31
193
|
this.subjectLocation = new Subject();
|
|
32
194
|
this.subjectNoResults = new Subject();
|
|
33
195
|
this.subjectRemove = new Subject();
|
|
34
196
|
this.http = inject(HttpClient);
|
|
197
|
+
this.config = inject(NGX_GEOCODING_CONFIG, {
|
|
198
|
+
optional: true,
|
|
199
|
+
});
|
|
200
|
+
/**
|
|
201
|
+
* Una sorgente che ha detto di no non si richiama per il resto della
|
|
202
|
+
* sessione : chiave senza il prodotto abilitato, referrer fuori dalle
|
|
203
|
+
* restrizioni, quota. Senza questo sarebbe una richiesta negata a ogni
|
|
204
|
+
* tasto premuto.
|
|
205
|
+
*/
|
|
206
|
+
this.spente = new Set();
|
|
207
|
+
/**
|
|
208
|
+
* Dentro `google`, i gradini già scesi : `AutocompleteSuggestion` (Places
|
|
209
|
+
* New) e `AutocompleteService` (Places legacy). Da lì in poi si parte
|
|
210
|
+
* direttamente da dove si era arrivati.
|
|
211
|
+
*/
|
|
212
|
+
this.suggestionSpenta = false;
|
|
213
|
+
this.placesSpenta = false;
|
|
35
214
|
}
|
|
36
215
|
/**
|
|
216
|
+
* Le proposte per quello che si sta digitando, dalla prima sorgente che
|
|
217
|
+
* risponde.
|
|
37
218
|
*
|
|
38
|
-
* @param address
|
|
219
|
+
* @param address il testo digitato
|
|
39
220
|
*/
|
|
40
221
|
geocode(address) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
222
|
+
return this.daSorgenti(address, this.sorgenti());
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Le coordinate del luogo **scelto**, chieste solo adesso.
|
|
226
|
+
*
|
|
227
|
+
* Le proposte di Places arrivano senza, e risolverle tutte a ogni tasto
|
|
228
|
+
* premuto vorrebbe dire una richiesta di dettaglio per proposta. Chi le ha
|
|
229
|
+
* già — Nominatim le manda sempre, e il `Geocoder` pure — non paga niente e
|
|
230
|
+
* si riprende il luogo così com'è, quindi **chiamarla è sempre corretto** :
|
|
231
|
+
* è la riga da mettere dove si sceglie una proposta, prima di usarne `lat`
|
|
232
|
+
* e `lon`.
|
|
233
|
+
*
|
|
234
|
+
* Fallendo restituisce il luogo com'è, senza coordinate : chi ha chiamato
|
|
235
|
+
* lo sa già gestire, ed è meglio che muovere una mappa a caso.
|
|
236
|
+
*
|
|
237
|
+
* @param place la proposta scelta fra quelle di {@link geocode}
|
|
238
|
+
*/
|
|
239
|
+
resolve(place) {
|
|
240
|
+
if (!place?.place_id || (place.lat && place.lon))
|
|
241
|
+
return of(place);
|
|
242
|
+
if (this.spente.has("google"))
|
|
243
|
+
return of(place);
|
|
244
|
+
if (!this.sorgenti().includes("google"))
|
|
245
|
+
return of(place);
|
|
246
|
+
// Lo stesso ordine delle proposte : chi le ha date sa dare anche il
|
|
247
|
+
// dettaglio, e chiedere il dettaglio a Places (New) quando le proposte
|
|
248
|
+
// sono arrivate dalla legacy sarebbe una richiesta negata in più.
|
|
249
|
+
if (!this.suggestionSpenta) {
|
|
250
|
+
return this.luogoDaSuggestion(place).pipe(catchError((err) => {
|
|
251
|
+
this.suggestionSpenta = true;
|
|
252
|
+
console.warn("[ngx-geocoding] dettaglio da Places (New) non " +
|
|
253
|
+
`riuscito, si prova la legacy — ${this.perche(err)}`);
|
|
254
|
+
return this.luogoDaLegacy(place);
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
if (this.placesSpenta)
|
|
258
|
+
return of(place);
|
|
259
|
+
return this.luogoDaLegacy(place);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Le coordinate con **Places (New)** : `Place` nasce con l'id e
|
|
263
|
+
* `fetchFields` lo **riempie**, quindi i campi si leggono dall'oggetto
|
|
264
|
+
* dopo l'attesa e non dal valore restituito.
|
|
265
|
+
*/
|
|
266
|
+
luogoDaSuggestion(place) {
|
|
267
|
+
return from(attendiPlaces()).pipe(switchMap((places) => {
|
|
268
|
+
const Luogo = places.Place;
|
|
269
|
+
if (!Luogo) {
|
|
270
|
+
return throwError(() => new Error("Place non c'è in questo SDK"));
|
|
271
|
+
}
|
|
272
|
+
const luogo = new Luogo({
|
|
273
|
+
id: place.place_id,
|
|
274
|
+
});
|
|
275
|
+
return from(luogo.fetchFields({
|
|
276
|
+
fields: ["location", "formattedAddress"],
|
|
277
|
+
})).pipe(map(() => ({
|
|
278
|
+
...place,
|
|
279
|
+
display_name: place.display_name ?? luogo.formattedAddress ?? "",
|
|
280
|
+
lat: this.testo(luogo.location?.lat()),
|
|
281
|
+
lon: this.testo(luogo.location?.lng()),
|
|
282
|
+
})));
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
/** Le coordinate con **Places legacy**, cioè `PlacesService.getDetails`. */
|
|
286
|
+
luogoDaLegacy(place) {
|
|
287
|
+
return from(attendiPlaces()).pipe(switchMap((places) => new Observable((chi) => {
|
|
288
|
+
new places.PlacesService(document.createElement("div")).getDetails({
|
|
289
|
+
placeId: place.place_id,
|
|
290
|
+
fields: ["geometry", "formatted_address"],
|
|
291
|
+
}, (luogo, stato) => {
|
|
292
|
+
if (stato !== STATO_PLACES.ok || !luogo) {
|
|
293
|
+
chi.error(new Error(stato));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const punto = luogo.geometry?.location;
|
|
297
|
+
chi.next({
|
|
298
|
+
...place,
|
|
299
|
+
display_name: place.display_name ??
|
|
300
|
+
luogo.formatted_address ??
|
|
301
|
+
"",
|
|
302
|
+
lat: this.testo(punto?.lat()),
|
|
303
|
+
lon: this.testo(punto?.lng()),
|
|
304
|
+
});
|
|
305
|
+
chi.complete();
|
|
306
|
+
});
|
|
307
|
+
})), catchError((err) => {
|
|
308
|
+
console.warn("[ngx-geocoding] coordinate del luogo non risolte — " +
|
|
309
|
+
this.perche(err));
|
|
310
|
+
return of(place);
|
|
311
|
+
}));
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Prova le sorgenti in ordine, passando alla successiva quando una rifiuta.
|
|
315
|
+
*
|
|
316
|
+
* Finite le sorgenti si rilancia un errore invece di restituire un elenco
|
|
317
|
+
* vuoto : «non ho trovato niente» e «non ho potuto cercare» sono due cose
|
|
318
|
+
* diverse, e chi mostra la barra deve poterle distinguere.
|
|
319
|
+
*/
|
|
320
|
+
daSorgenti(address, restanti) {
|
|
321
|
+
const sorgente = restanti[0];
|
|
322
|
+
if (!sorgente) {
|
|
323
|
+
return throwError(() => new Error("Nessuna sorgente di geocoding disponibile"));
|
|
324
|
+
}
|
|
325
|
+
const dopo = restanti.slice(1);
|
|
326
|
+
if (this.spente.has(sorgente))
|
|
327
|
+
return this.daSorgenti(address, dopo);
|
|
328
|
+
const richiesta = sorgente === "google"
|
|
329
|
+
? this.daGoogle(address)
|
|
330
|
+
: this.daNominatim(address);
|
|
331
|
+
return richiesta.pipe(tap((luoghi) => {
|
|
332
|
+
if (luoghi.length <= 0) {
|
|
46
333
|
this.subjectNoResults.next(null);
|
|
47
334
|
}
|
|
48
|
-
|
|
49
|
-
|
|
335
|
+
}), catchError((err) => {
|
|
336
|
+
this.spente.add(sorgente);
|
|
337
|
+
console.warn(`[ngx-geocoding] sorgente "${sorgente}" non utilizzabile, ` +
|
|
338
|
+
`non la si richiede più — ${this.perche(err)}`);
|
|
339
|
+
return this.daSorgenti(address, dopo);
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Nominatim, **identico a come questa libreria l'ha sempre chiamato** : è
|
|
344
|
+
* il comportamento su cui contano le applicazioni che la usavano prima che
|
|
345
|
+
* Google esistesse qui dentro.
|
|
346
|
+
*/
|
|
347
|
+
daNominatim(address) {
|
|
348
|
+
const encoded = encodeURIComponent(address);
|
|
349
|
+
const base = this.config?.nominatimUrl ?? NOMINATIM;
|
|
350
|
+
return this.http
|
|
351
|
+
.get(`${base}?q=${encoded}&format=jsonv2`)
|
|
50
352
|
.pipe(map((res) => res.map((item) => ({
|
|
51
353
|
display_name: item.display_name,
|
|
52
354
|
lat: item.lat,
|
|
@@ -54,16 +356,222 @@ class NgxGeocodingService {
|
|
|
54
356
|
place_id: item.place_id,
|
|
55
357
|
}))));
|
|
56
358
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Google : le proposte di Places, e il `Geocoder` quando Places dice di no.
|
|
361
|
+
*
|
|
362
|
+
* `ZERO_RESULTS` **non** è un rifiuto : è una ricerca senza esiti, e
|
|
363
|
+
* confonderlo con un errore farebbe scivolare al `Geocoder` — e poi fuori
|
|
364
|
+
* da Google — alla prima ricerca sfortunata.
|
|
365
|
+
*/
|
|
366
|
+
daGoogle(address) {
|
|
367
|
+
if (this.suggestionSpenta)
|
|
368
|
+
return this.dallaLegacy(address);
|
|
369
|
+
return this.daSuggestion(address).pipe(catchError((err) => {
|
|
370
|
+
this.suggestionSpenta = true;
|
|
371
|
+
console.warn("[ngx-geocoding] Places (New) non utilizzabile, si prova " +
|
|
372
|
+
`quella legacy — ${this.perche(err)}`);
|
|
373
|
+
return this.dallaLegacy(address);
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
/** Il secondo e il terzo gradino : Places legacy, poi il `Geocoder`. */
|
|
377
|
+
dallaLegacy(address) {
|
|
378
|
+
if (this.placesSpenta)
|
|
379
|
+
return this.daGeocoder(address);
|
|
380
|
+
return this.daPlaces(address).pipe(catchError((err) => {
|
|
381
|
+
this.placesSpenta = true;
|
|
382
|
+
console.warn("[ngx-geocoding] Places legacy non utilizzabile, si prova " +
|
|
383
|
+
`il Geocoder — ${this.perche(err)}`);
|
|
384
|
+
return this.daGeocoder(address);
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Le proposte di **Places (New)**, con `AutocompleteSuggestion`.
|
|
389
|
+
*
|
|
390
|
+
* Due cose che non si vedono dalla firma : `text` di una proposta non è una
|
|
391
|
+
* stringa ma un oggetto formattabile, e va letto con `toString()` — messo
|
|
392
|
+
* in un template darebbe `[object Object]` ; e un elenco **vuoto non è un
|
|
393
|
+
* errore**, è una ricerca senza esiti, quindi non fa scendere di gradino.
|
|
394
|
+
*
|
|
395
|
+
* Se la classe non c'è nell'SDK — versione più vecchia — si solleva subito,
|
|
396
|
+
* che è il modo di scendere senza aspettare una risposta che non arriverà.
|
|
397
|
+
*/
|
|
398
|
+
daSuggestion(address) {
|
|
399
|
+
const paese = this.config?.country;
|
|
400
|
+
const cerchio = this.cerchio();
|
|
401
|
+
return from(attendiPlaces()).pipe(switchMap((places) => {
|
|
402
|
+
const nuova = places.AutocompleteSuggestion;
|
|
403
|
+
if (!nuova) {
|
|
404
|
+
return throwError(() => new Error("AutocompleteSuggestion non c'è in questo SDK"));
|
|
405
|
+
}
|
|
406
|
+
return from(nuova.fetchAutocompleteSuggestions({
|
|
407
|
+
input: address,
|
|
408
|
+
...(paese
|
|
409
|
+
? { includedRegionCodes: [paese], region: paese }
|
|
410
|
+
: {}),
|
|
411
|
+
...(cerchio ? { locationBias: cerchio } : {}),
|
|
412
|
+
})).pipe(map((risposta) => (risposta?.suggestions ?? [])
|
|
413
|
+
.map((s) => s.placePrediction)
|
|
414
|
+
.filter((p) => !!p?.placeId)
|
|
415
|
+
.map((p) => ({
|
|
416
|
+
place_id: p.placeId,
|
|
417
|
+
display_name: p.text?.toString() ?? "",
|
|
418
|
+
}))));
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
/** Le proposte incrementali di `AutocompleteService`. */
|
|
422
|
+
daPlaces(address) {
|
|
423
|
+
const paese = this.config?.country;
|
|
424
|
+
const cerchio = this.cerchio();
|
|
425
|
+
return from(attendiPlaces()).pipe(switchMap((places) => new Observable((chi) => {
|
|
426
|
+
new places.AutocompleteService().getPlacePredictions({
|
|
427
|
+
input: address,
|
|
428
|
+
...(paese
|
|
429
|
+
? {
|
|
430
|
+
componentRestrictions: {
|
|
431
|
+
country: paese,
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
: {}),
|
|
435
|
+
...(cerchio ? { locationBias: cerchio } : {}),
|
|
436
|
+
}, (proposte, stato) => {
|
|
437
|
+
if (stato === STATO_PLACES.nessuno) {
|
|
438
|
+
chi.next([]);
|
|
439
|
+
chi.complete();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (stato !== STATO_PLACES.ok) {
|
|
443
|
+
chi.error(new Error(stato));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
chi.next((proposte ?? [])
|
|
447
|
+
.filter((p) => !!p.place_id)
|
|
448
|
+
.map((p) => ({
|
|
449
|
+
place_id: p.place_id,
|
|
450
|
+
display_name: p.description ?? "",
|
|
451
|
+
})));
|
|
452
|
+
chi.complete();
|
|
453
|
+
});
|
|
454
|
+
})));
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Il `Geocoder`, l'ultima strada di Google.
|
|
458
|
+
*
|
|
459
|
+
* I suoi risultati **portano già le coordinate**, quindi su questi
|
|
460
|
+
* {@link resolve} non fa nessuna richiesta in più.
|
|
461
|
+
*/
|
|
462
|
+
daGeocoder(address) {
|
|
463
|
+
const paese = this.config?.country;
|
|
464
|
+
const riquadro = this.riquadro();
|
|
465
|
+
return from(attendiGeocoder()).pipe(switchMap((geocoder) => new Observable((chi) => {
|
|
466
|
+
geocoder.geocode({
|
|
467
|
+
address,
|
|
468
|
+
...(paese
|
|
469
|
+
? {
|
|
470
|
+
componentRestrictions: {
|
|
471
|
+
country: paese,
|
|
472
|
+
},
|
|
473
|
+
region: paese,
|
|
474
|
+
}
|
|
475
|
+
: {}),
|
|
476
|
+
...(riquadro ? { bounds: riquadro } : {}),
|
|
477
|
+
}, (risultati, stato) => {
|
|
478
|
+
if (stato === STATO_PLACES.nessuno) {
|
|
479
|
+
chi.next([]);
|
|
480
|
+
chi.complete();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (stato !== STATO_PLACES.ok) {
|
|
484
|
+
chi.error(new Error(stato));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
chi.next((risultati ?? []).map((r) => ({
|
|
488
|
+
place_id: r.place_id,
|
|
489
|
+
display_name: r.formatted_address ?? "",
|
|
490
|
+
lat: this.testo(r.geometry?.location?.lat()),
|
|
491
|
+
lon: this.testo(r.geometry?.location?.lng()),
|
|
492
|
+
})));
|
|
493
|
+
chi.complete();
|
|
494
|
+
});
|
|
495
|
+
})));
|
|
496
|
+
}
|
|
497
|
+
/** Il bias come lo vuole `AutocompleteService`. */
|
|
498
|
+
cerchio() {
|
|
499
|
+
const bias = this.config?.bias;
|
|
500
|
+
return bias
|
|
501
|
+
? {
|
|
502
|
+
center: { lat: bias.lat, lng: bias.lon },
|
|
503
|
+
radius: bias.radiusM,
|
|
504
|
+
}
|
|
505
|
+
: undefined;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Il bias come lo vuole il `Geocoder`, che prende un **riquadro** e non un
|
|
509
|
+
* cerchio : lo si iscrive attorno al punto. La longitudine si stringe con
|
|
510
|
+
* la latitudine (`cos`), o alle nostre latitudini il riquadro verrebbe
|
|
511
|
+
* largo un terzo in più del voluto.
|
|
512
|
+
*/
|
|
513
|
+
riquadro() {
|
|
514
|
+
const bias = this.config?.bias;
|
|
515
|
+
if (!bias)
|
|
516
|
+
return undefined;
|
|
517
|
+
const dLat = bias.radiusM / METRI_PER_GRADO;
|
|
518
|
+
const dLon = dLat / Math.max(Math.cos((bias.lat * Math.PI) / 180), 0.01);
|
|
519
|
+
return {
|
|
520
|
+
north: bias.lat + dLat,
|
|
521
|
+
south: bias.lat - dLat,
|
|
522
|
+
east: bias.lon + dLon,
|
|
523
|
+
west: bias.lon - dLon,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
/** Le sorgenti dichiarate, o quella storica. */
|
|
527
|
+
sorgenti() {
|
|
528
|
+
const dichiarate = this.config?.sources ?? [];
|
|
529
|
+
return dichiarate.length ? dichiarate : SOLO_NOMINATIM;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Perché una strada ha detto di no, in una riga : dall'SDK arriva uno stato
|
|
533
|
+
* (`REQUEST_DENIED` per la chiave senza il prodotto abilitato o per il
|
|
534
|
+
* referrer, `OVER_QUERY_LIMIT` per la quota), da una http il corpo
|
|
535
|
+
* d'errore. Senza questo il motivo resta dentro un oggetto che in console
|
|
536
|
+
* va aperto, e si legge solo «si ripiega».
|
|
537
|
+
*/
|
|
538
|
+
perche(err) {
|
|
539
|
+
if (err instanceof Error && err.message)
|
|
540
|
+
return err.message;
|
|
541
|
+
const dettaglio = err?.error?.error;
|
|
542
|
+
if (!dettaglio)
|
|
543
|
+
return "motivo non dichiarato";
|
|
544
|
+
return [dettaglio.status, dettaglio.message]
|
|
545
|
+
.filter((pezzo) => !!pezzo)
|
|
546
|
+
.join(" : ");
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Una coordinata come stringa, o `undefined` se non c'è.
|
|
550
|
+
*
|
|
551
|
+
* `String(undefined)` darebbe `"undefined"`, che passerebbe indenne per
|
|
552
|
+
* ogni controllo di presenza e morirebbe in `Number()` — cioè una mappa
|
|
553
|
+
* portata su `NaN, NaN` senza che niente lo dica.
|
|
554
|
+
*/
|
|
555
|
+
testo(valore) {
|
|
556
|
+
return valore === null || valore === undefined
|
|
557
|
+
? undefined
|
|
558
|
+
: String(valore);
|
|
559
|
+
}
|
|
560
|
+
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgxGeocodingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
561
|
+
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgxGeocodingService, providedIn: "root" }); }
|
|
59
562
|
}
|
|
60
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
563
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: NgxGeocodingService, decorators: [{
|
|
61
564
|
type: Injectable,
|
|
62
565
|
args: [{
|
|
63
566
|
providedIn: "root",
|
|
64
567
|
}]
|
|
65
568
|
}] });
|
|
66
569
|
|
|
570
|
+
/**
|
|
571
|
+
* @author Vito Salvia - CNR IMAA geoSDI Group
|
|
572
|
+
* @email vito.salvia@gmail.com
|
|
573
|
+
*/
|
|
574
|
+
|
|
67
575
|
/*
|
|
68
576
|
* Public API Surface of ngx-geocoding
|
|
69
577
|
*/
|
|
@@ -72,5 +580,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.6", ngImpor
|
|
|
72
580
|
* Generated bundle index. Do not edit.
|
|
73
581
|
*/
|
|
74
582
|
|
|
75
|
-
export { NgxGeocodingModule, NgxGeocodingService };
|
|
583
|
+
export { NGX_GEOCODING_CONFIG, NgxGeocodingModule, NgxGeocodingService, provideNgxGeocoding };
|
|
76
584
|
//# sourceMappingURL=geosdi-ngx-geocoding.mjs.map
|