Skip to main content

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/geolocation plugin.
  • Browser: navigator.geolocation with enableHighAccuracy: true and 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

MethodSignatureDescription
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?): stringBuilds 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 / PropertySignatureDescription
CanGeolocateboolean (getter)true if the platform supports geolocation. Always true on Cordova; checks navigator.geolocation on web.
GetCurrentPosition(resolve, reject)(resolve, reject): voidOne-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.
CurrentPositionWatchPosition (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): numberCalculates 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

MethodSignatureDescription
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

ObservableDescription
OnPositionChangedEmits 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

  1. If geolocation permission has not been previously granted, NearBy shows an alert explaining why geolocation is needed.
  2. After the user acknowledges, StartWatch() is called, which triggers the browser's permission prompt.
  3. If permission is denied, the appropriate error toast is shown and NearBy returns false.
  4. 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.
  • RequestTimezne is intentionally misspelled: the method name in the codebase is RequestTimezne (missing an 'o'). Use this exact name.
  • DistanceTo uses 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 modalAwayComponent is presented as a full-screen modal with backdropDismiss: false. The user must either move within range or manually close the modal.
  • Android permissions: on Android, the service automatically requests ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions 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 (the UiAwayComponent), not for the REST geocoding APIs.
ServiceRelationship
httpServiceUsed for Google API REST calls (geocoding, timezone).
platformServiceDetermines whether to use Cordova or browser geolocation.
alertServiceShows the geolocation permission request dialog.
toastServiceShows error toasts for geolocation failures and Google API errors.
languageServiceTranslates error messages and the Away component's labels.
viewServiceUiAwayComponent uses viewService for online status.