How the Atlas World Map Addon Tracks Visited Countries in TREK
The Atlas world map addon tracks visited countries through a hybrid approach that combines manual user markings stored in SQLite tables with automatic derivation from trip place data, utilizing background geocoding and multi-layer caching to resolve location coordinates to country codes.
The Atlas addon in the TREK repository visualizes an interactive world map that highlights every country or sub-national region a user has visited. According to the TREK source code, this tracking system employs both explicit manual markings and automatic resolution from place coordinates attached to user trips, storing results in a normalized SQLite schema and exposing aggregated data via REST endpoints.
Data Storage Architecture for Country Tracking
The tracking system relies on three core tables defined in shared/src/atlas/atlas.schema.ts and accessed through src/db/database.ts.
visited_countries– Stores manual user markings with columnsuser_id,country_code, andcreated_at.visited_regions– Optional sub-national granularity withuser_id,country_code,region_code,region_name, andcreated_at.place_regions– Caches reverse-geocode results for trip places, linkingplace_idto resolvedcountry_codeandregion_code.
This schema separates persistent user intent (manual marks) from derived trip data (automatic resolution), allowing the system to merge both sources at query time.
Manual Marking Flow
Users can explicitly add or remove countries via the Atlas UI, which triggers a synchronized client-server flow.
API Endpoints for Marking Countries
The NestJS controller in src/nest/atlas/atlas.controller.ts exposes endpoints that delegate to the service layer:
// src/nest/atlas/atlas.controller.ts
@Patch(':code')
async markVisited(@Param('code') code: string, @UserId() userId: number) {
this.atlasService.markCountryVisited(userId, code.toUpperCase());
}
The client initiates these requests through client/src/pages/atlas/atlasModel.ts:
// client/src/pages/atlas/atlasModel.ts
await fetch(`/api/atlas/visited-countries/${countryCode}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
Database Operations
In src/services/atlasService.ts, the marking functions execute idempotent SQL operations:
// src/services/atlasService.ts
export function markCountryVisited(userId: number, code: string): void {
db.prepare('INSERT OR IGNORE INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(userId, code);
}
export function unmarkCountryVisited(userId: number, code: string): void {
db.prepare('DELETE FROM visited_countries WHERE user_id = ? AND country_code = ?').run(userId, code);
}
The INSERT OR IGNORE pattern prevents duplicate entries, while the DELETE statement removes explicit markings without affecting automatically derived trip data.
Automatic Derivation from Trip Places
When generating statistics, Atlas automatically resolves every place attached to a user’s trips into country codes, then aggregates the results.
Resolving Places to Country Codes
The resolvePlaceCountries function in src/services/atlasService.ts processes each place through resolveCountryCodeSync, which attempts resolution in this order:
getCountryFromAddress– Parses the address string for a country code.getCountryFromCoords– Performs a bounding-box pre-filter (COUNTRY_BOXES) followed by an exact polygon test for lat/long coordinates.reverseGeocodeCountry– Enqueues a background Nominatim API request if cache misses occur, storing results inplace_regions.
Aggregation and Merging Logic
The getStats function aggregates resolved places into a countrySet Map, then merges manual entries:
// src/services/atlasService.ts – aggregation core
export async function getStats(userId: number) {
const trips = getUserTrips(userId);
const places = getPlacesForTrips(trips.map(t => t.id));
const placeCountries = resolvePlaceCountries(places);
const countrySet = new Map<string, CountryEntry>();
for (const place of places) {
const code = placeCountries.get(place.id);
if (code) {
if (!countrySet.has(code)) countrySet.set(code, { code, places: [], tripIds: new Set() });
const entry = countrySet.get(code)!;
entry.places.push({ id: place.id, name: place.name, lat: place.lat ?? null, lng: place.lng ?? null });
entry.tripIds.add(place.trip_id);
}
}
// Merge manual marks
const manual = db.prepare('SELECT country_code FROM visited_countries WHERE user_id = ?')
.all(userId) as { country_code: string }[];
for (const mc of manual) {
if (!countrySet.has(mc.country_code)) {
countrySet.set(mc.country_code, { code: mc.country_code, places: [], tripIds: new Set() });
}
}
// ...
}
This ensures that manually marked countries appear in the final dataset even when no geocoded places exist for them.
Background Geocoding and Caching Strategy
To minimize external API calls, Atlas implements a multi-layer caching system.
The Geo Bundle and Spatial Lookups
On startup, the service loads a pre-built gzipped GeoJSON bundle (loadGeoBundle) containing admin-0 (countries) and admin-1 (regions) geometries. The COUNTRY_BOXES constant provides fast bounding-box pre-filtering before expensive polygon inclusion tests.
Persistent Caching Layers
Three cache layers prevent redundant geocoding:
geocodeCache– In-memory Map for recent lat-lng to country lookups.regionCache– In-memory Map for lat-lng to region lookups.place_regionstable – Persistent SQLite storage for resolved place coordinates.
When synchronous resolution fails, the system enqueues background requests:
// background task in resolvePlaceCountries()
if (p.lat && p.lng && !geocodingInFlight.has(p.id)) {
uncachedForGeocode.push(p);
}
The background worker updates place_regions upon completion and clears the in-flight tracking set.
Client-Side API Consumption
The client consumes aggregated data through two primary endpoints defined in src/nest/atlas/atlas.controller.ts.
The statistics endpoint returns the complete visited list:
// client/src/pages/atlas/atlasModel.ts
const resp = await fetch('/api/atlas/stats');
const { countries } = await resp.json();
// Each item: {code, placeCount, tripCount, manually_marked?, ...}
The manually_marked boolean flag indicates entries sourced from visited_countries rather than automatic derivation. For country-specific details, the client calls GET /atlas/:code, which invokes getCountryPlaces to list all associated places and trips.
Summary
- Hybrid tracking combines explicit manual markings in
visited_countrieswith automatic resolution from trip place coordinates. - Manual control allows users to add or remove countries via
markCountryVisitedandunmarkCountryVisitedinsrc/services/atlasService.ts. - Automatic derivation uses
resolveCountryCodeSyncwith address parsing, bounding-box filters, and polygon tests to map places to country codes. - Triple caching via
geocodeCache,regionCache, and theplace_regionstable eliminates redundant Nominatim API calls. - REST endpoints in
atlas.controller.tsserve merged data to the Leaflet map interface, distinguishing manual marks with amanually_markedflag.
Frequently Asked Questions
How does Atlas handle countries when place coordinates are missing?
When coordinates are absent or resolution fails, the system relies solely on the visited_countries table for that user. If a user has manually marked the country, it appears in the statistics; otherwise, it is omitted until geocoding completes or the user adds the mark manually.
Can users mark sub-national regions as visited?
Yes, the schema supports region-level tracking through the visited_regions table, which stores region_code and region_name alongside country_code. The same manual marking flow applies, allowing the Atlas map to highlight specific states or provinces.
What happens if a user manually marks a country that also has trip places?
The aggregation logic in getStats merges both sources. The country appears in the result set with manually_marked: true, and the associated places from trips are appended to the entry’s places array. This prevents duplicates while preserving the distinction between automatic and manual tracking.
How does the system avoid hitting Nominatim API rate limits?
Atlas employs a three-tier caching strategy: in-memory Maps for recent lookups, a persistent place_regions table for resolved coordinates, and a background queue that processes only uncached places asynchronously. Places currently being geocoded are tracked in geocodingInFlight to prevent duplicate requests.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →