Building a live fleet map on the Navixy GPS API
Polling that stops when nobody is looking, markers that glide instead of jumping, and the small traps in GPS data: two kinds of status, and trackers that report from 0,0.
Ahmed Abdelmageed
3 min read
Sima Track is a fleet tracking platform: every vehicle live on a map, on the web and in an iOS and Android app. The positions come from Navixy, a GPS tracking platform with an HTTP API. Navixy hands you coordinates. Making them feel like a live map is the app's job, and these are the parts that took thought.
1. Keep the account key on a server#
Navixy accepts two kinds of credential: an API key for the whole account, and a session hash that a user gets by logging in. The web dashboard sees neither. The browser talks to an Express backend with its own login, and the backend adds the key and calls Navixy.
this.http = axios.create({
baseURL,
headers: { Authorization: `NVX ${apiKey}` },
timeout: 15_000,
});The mobile app works differently. Each user signs in with their own Navixy login, and the app keeps only that user's session hash, in the phone's secure storage. The account key is never shipped inside the app, where anyone could dig it out.
2. Poll, but only while someone is looking#
Positions are fetched by polling every five seconds. A plain setInterval has a cost on a phone: a tab stays mounted after you leave it, so a bare timer keeps fetching every vehicle and redrawing every marker underneath whatever screen is open. The app's polling hook runs only while its screen is focused, and skips ticks when the app is in the background.
export function usePoll(fn: () => void | Promise<unknown>, ms: number) {
const saved = useRef(fn);
useEffect(() => {
saved.current = fn;
}, [fn]);
useFocusEffect(
useCallback(() => {
const tick = () => {
if (AppState.currentState !== 'active') return;
Promise.resolve(saved.current()).catch(() => {});
};
tick();
const id = setInterval(tick, ms);
return () => clearInterval(id);
}, [ms]),
);
}One more trap: the first request after the phone wakes often fails, because the system closed the connection while the app was suspended. The client sends a failed request once more after 600 ms, and only for read endpoints. A write that got no answer may still have been applied, so it is never repeated.
const READ = /\/(list|list_\w+|get_\w+|count|search|search_\w+)$/;
export function isReplayable(url: string): boolean {
return READ.test(url);
}3. Two kinds of status, and a tracker at 0,0#
Navixy reports two separate things about a tracker: whether it is connected, and whether it is moving. A map needs one colour per vehicle, so the app folds them into one status, connection first.
function mapStatus(s: NavixyState): VehicleStatus {
const conn = s.connection_status;
if (!conn || conn === 'offline' || conn === 'signal_lost' || conn === 'just_registered') {
return 'OFFLINE';
}
if (s.movement_status === 'moving') return 'MOVING';
if (s.movement_status === 'parked' || s.movement_status === 'stopped') return 'STOPPED';
return 'IDLE';
}A parked car that has gone offline is offline, not parked.
The other surprise is a tracker that has never had a GPS fix. It reports a location of 0,0 with no timestamp, and drawn as it is, that puts a vehicle in the ocean off West Africa. The app treats it as having no position and leaves it off the map.
const loc = s.gps?.location;
// A tracker with no fix (e.g. just_registered) returns location {0,0} with a
// null `updated` — treat that as no position, not a marker off West Africa.
if (!loc || !s.gps?.updated) return null;4. Make five-second updates look like movement#
With a new position every five seconds, a marker that jumps looks broken. On the phone each marker's coordinate is animated, so a new fix glides the vehicle to its place over 900 ms.
const coord = useRef(
new AnimatedRegion({
latitude: pos.lat,
longitude: pos.lng,
latitudeDelta: 0,
longitudeDelta: 0,
}),
).current;
useEffect(() => {
coord
.timing({
latitude: pos.lat,
longitude: pos.lng,
latitudeDelta: 0,
longitudeDelta: 0,
duration: 900,
useNativeDriver: false,
} as any)
.start();
// ...There is a catch with custom markers in react-native-maps: the marker's view is turned into a bitmap once, so a change of heading or status never shows. The fix is to let the marker redraw for one second after each update and then freeze it again, which keeps a map full of markers cheap. And when the camera follows a selected vehicle, it moves the centre only, so the zoom the user chose stays put.
What I'd tell anyone building this#
- Keep the account key on a server, and give each phone a per-user session.
- Poll only while the screen is focused and the app is active.
- Retry reads once. Never retry writes.
- Decide what "status" means before picking colours, and guard against 0,0.
- Animate between fixes; a map that jumps reads as a bug.


