Geocode Service
Introduction
geocodeService provides geolocation and geocoding functionality for the application, integrating with the browser Geolocation API, the Cordova Geolocation plugin, and several Google Maps APIs (Geocoding, Timezone, Static Maps). It exists because the application needs to resolve addresses for places (establishments), determine timezones for correct time display, enforce proximity restrictions (requiring users to be physically near a place to operate), and display maps showing the user's position relative to a place.
When to use
- Place setup: when creating or editing a place, resolve an address to coordinates or reverse-geocode coordinates to an address.
- Timezone determination: get the IANA timezone for a place's location so the application displays times correctly.
- Proximity restriction (NearBy): enforce that the user must be within a certain distance of the place to use the application (e.g. for employee clock-in).
- Static map images: generate Google Static Maps URLs for preview images in lists or cards.
- Distance calculation: compute the distance between the user's current position and a set of coordinates.
How it works
The service has three main subsystems:
1. Geocoding (Google Maps API)
All geocoding goes through Google's REST APIs using httpService:
RequestAddress(lat, lng)
│
▼
GET https://maps.googleapis.com/maps/api/geocode/json?key=KEY&latlng=lat,lng
│
▼
_parseGoogleResponse() → Address object
│
▼
Cache the result (in-memory Map, keyed by "lat,lng")
Results are cached in-memory Map instances (_requestCache, _resolveCache, _timezneCache) to avoid redundant API calls. The cache persists for the lifetime of the service (the entire session).
2. Geolocation (Browser / Cordova)
Position retrieval uses the platform-appropriate API:
- Cordova:
@awesome-cordova-plugins/geolocationplugin. - Browser:
navigator.geolocationwithenableHighAccuracy: trueand a 5-second timeout.
For continuous tracking, StartWatch() sets up a watch that updates CurrentPosition on every position change and emits OnPositionChanged.
3. NearBy proximity restriction
The NearBy system combines geolocation watching with a modal overlay:
NearBy(center, radius)
│
▼
Permission granted? ──no──► AskPermission() → show alert → StartWatch()
│ yes
▼
StartWatch()
│
▼
WaitForNear(center, radius)
│
├── IsNear(center, radius)? ──yes──► return true
│
└── no ──► ShowAway(center) → modalAwayComponent
│ monitors position via OnPositionChanged
│ auto-dismisses when distance < radius + accuracy
▼
return true (when near) or false (if user closes modal)
import { geocodeService, Address, Coordinates, WatchPosition } from '@unpispas/upp-base';
constructor(private geocode: geocodeService) {}
Types
interface Coordinates {
lat: number;
lng: number;
}
interface WatchPosition {
granted: boolean; // whether geolocation permission was granted
coordinates: {
lat: number;
lng: number;
};
accuracy: number; // accuracy in metres
}
interface Address {
formatted: string; // full formatted address string
location: { lat: number; lng: number };
number: string; // street number
street: {
route: string; // street/road name
locality: string; // city/town
};
postal_code: string;
area: {
level1: string; // administrative area level 1 (e.g. autonomous community)
level2: string; // level 2 (e.g. province)
level3: string; // country short code
};
timezone: string; // IANA timezone (e.g. 'Europe/Madrid')
}
interface GeocodePosition {
coords: {
latitude: number;
longitude: number;
altitude: number | null;
accuracy: number;
altitudeAccuracy: number | null;
heading: number | null;
speed: number | null;
};
timestamp: number;
}
API Reference
Geocoding methods
| Method | Signature | Description |
|---|---|---|
RequestAddress(lat, lng) | (lat: number, lng: number): Promise<Address | null> | Reverse geocode: returns a structured Address for the given coordinates. Results are cached in memory. Returns null on error. Shows a danger toast if the Google API returns an error other than ZERO_RESULTS. |
ResolveAddress(address) | (address: string): Promise<Address | null> | Forward geocode: resolves a text query (e.g. "Bilbao, Spain") to an Address. Results are cached. Returns null on error. |
RequestTimezne(lat, lng) | (lat: number, lng: number): Promise<string> | Returns the IANA timezone ID for the given coordinates (e.g. 'Europe/Madrid'). Falls back to 'UTC' on error. Results are cached. Uses the Google Timezone API with the current timestamp. |
StaticUrl(lat, lng, width, height, zoom?) | (lat, lng, width, height, zoom?): string | Builds a Google Static Maps URL for a map image with a red marker at the specified location. Default zoom: 15. Returns a URL string suitable for use in an <img src="...">. |
Geolocation methods
| Method / Property | Signature | Description |
|---|---|---|
CanGeolocate | boolean (getter) | true if the platform supports geolocation. Always true on Cordova; checks navigator.geolocation on web. |
GetCurrentPosition(resolve, reject) | (resolve, reject): void | One-shot position retrieval. Uses the Cordova plugin or browser API with enableHighAccuracy: true, 5,000 ms timeout. The callback-based API is designed for compatibility with both platforms. |
CurrentPosition | WatchPosition (getter) | The most recent watched position. Updated continuously when a watch is active. Initially { granted: false, coordinates: { lat: 0, lng: 0 }, accuracy: 0 }. |
DistanceTo(coordinates) | (coordinates: Coordinates): number | Calculates the distance in metres from CurrentPosition to the given coordinates using the Haversine formula. Returns an absolute rounded integer. |
LoadGoogleMaps(loaded) | (loaded: Subject<boolean>): Promise<void> | Dynamically loads the Google Maps JavaScript API (for interactive maps, not the REST APIs). Sets GMAP_API_LOADED = true on success. The loaded Subject emits true when the API is ready. |
NearBy proximity restriction
| Method | Signature | Description |
|---|---|---|
NearBy(center, radius?) | (center: Coordinates | null, radius?: number): Promise<boolean> | Enables or disables proximity restriction. Pass null as center to disable. Default radius: 250 m. Returns true if the user is within range or restriction is disabled. Returns false if the user denied geolocation or could not be located. |
WaitForNear(center, radius?) | (center: Coordinates, radius?: number): Promise<boolean> | Waits until the user is within range. If outside, presents the modalAwayComponent which shows a map with the user's position and the target. The modal auto-dismisses when the user enters the allowed radius. Returns true on success, false if the user manually closes the modal. |
Observables
| Observable | Description |
|---|---|
OnPositionChanged | Emits when the watched position updates. Only fires when a position watch is active (started by NearBy). |
Components
modalAwayComponent
Selector: upp-modal-away. A full-screen modal displayed when the user is too far from the target location. It receives center: Coordinates as input and listens to distance changes from UiAwayComponent. Auto-dismisses when distance < inrange + accuracy (default inrange: 250 m). The user can also close it manually, which returns false from WaitForNear.
UiAwayComponent
Selector: upp-away. Displays a Google Map (using the JavaScript API) with two markers:
- A custom marker for the place center (using
/assets/image/marker-place.png). - A default marker for the user's current position.
The component calculates the distance, adjusts the zoom to fit both markers, and displays the distance in metres or kilometres. It extends languageSupport for translated labels. The changeEvent emitter notifies the parent (modalAwayComponent) with { distance, accuracy } on every position update.
Usage examples
Reverse geocode to fill in place address fields
const address = await this.geocode.RequestAddress(43.2630, -2.9350);
if (address) {
this.place.address = address.formatted; // "Bilbao, Spain"
this.place.city = address.street.locality; // "Bilbao"
this.place.province = address.area.level2; // "Bizkaia"
this.place.country = address.area.level3; // "ES"
this.place.postalCode = address.postal_code;
this.place.coordinates = address.location;
}
Forward geocode from a search box
async onAddressSearch(query: string) {
const address = await this.geocode.ResolveAddress(query);
if (address) {
this.map.center = address.location;
this.place.timezone = address.timezone || await this.geocode.RequestTimezne(
address.location.lat, address.location.lng
);
}
}
Get timezone for correct time display
const tz = await this.geocode.RequestTimezne(43.2630, -2.9350);
// tz = "Europe/Madrid"
const localTime = new Date().toLocaleString('es-ES', { timeZone: tz });
Show a static map preview in a card
const mapUrl = this.geocode.StaticUrl(43.2630, -2.9350, 300, 200, 14);
// Use in template: <img [src]="mapUrl" />
Enable proximity restriction for employee clock-in
const placeCoords: Coordinates = { lat: 43.2630, lng: -2.9350 };
// Enable 250m restriction -- prompts for geolocation permission if needed
const isNear = await this.geocode.NearBy(placeCoords, 250);
if (isNear) {
await this.clockIn();
} else {
this.toast.ShowAlert('warning', 'You must be near the workplace to clock in');
}
// Later, when the user logs out, disable the restriction
await this.geocode.NearBy(null);
Calculate distance for a list of places
for (const place of this.places) {
place.distance = this.geocode.DistanceTo({
lat: place.latitude,
lng: place.longitude
});
}
// Sort by distance
this.places.sort((a, b) => a.distance - b.distance);
Common patterns
Caching behaviour
All geocoding results are cached in in-memory Maps for the duration of the session. This means:
- The first call for a given lat/lng pair makes an HTTP request to Google.
- Subsequent calls with the same coordinates return instantly from cache.
- The cache is not persisted -- it resets on page reload.
For timezones, the cache key uses coordinates truncated to 4 decimal places (about 11m precision), so nearby points share the same timezone result.
Permission flow for NearBy
- If geolocation permission has not been previously granted,
NearByshows an alert explaining why geolocation is needed. - After the user acknowledges,
StartWatch()is called, which triggers the browser's permission prompt. - If permission is denied, the appropriate error toast is shown and
NearByreturnsfalse. - If permission is granted, the position watch starts and the proximity check proceeds.
Gotchas and tips
- Google API key is required: all Google Maps API calls use
AppConstants.googleApiKey. If this key is missing, invalid, or has exhausted its quota, geocoding and timezone requests will fail. RequestTimezneis intentionally misspelled: the method name in the codebase isRequestTimezne(missing an 'o'). Use this exact name.DistanceTouses the Haversine formula: it returns the great-circle distance, which is accurate for points up to a few hundred kilometres apart. The result is in metres, rounded to the nearest integer.- NearBy modal blocks the UI: when the user is outside the radius, the
modalAwayComponentis presented as a full-screen modal withbackdropDismiss: false. The user must either move within range or manually close the modal. - Android permissions: on Android, the service automatically requests
ACCESS_FINE_LOCATIONandACCESS_COARSE_LOCATIONpermissions on construction. On iOS and web, the browser handles permissions natively. - Google Maps JavaScript API is loaded on demand:
LoadGoogleMaps()dynamically adds a<script>tag. It is only needed for interactive maps (theUiAwayComponent), not for the REST geocoding APIs.
Related services
| Service | Relationship |
|---|---|
httpService | Used for Google API REST calls (geocoding, timezone). |
platformService | Determines whether to use Cordova or browser geolocation. |
alertService | Shows the geolocation permission request dialog. |
toastService | Shows error toasts for geolocation failures and Google API errors. |
languageService | Translates error messages and the Away component's labels. |
viewService | UiAwayComponent uses viewService for online status. |