October 10, 2025
Check out My Maps Pedometer app here! MapPedometer
As I moved to a new area, I kept asking myself "what's a good road to go for a quick walk/jog down?". So, I opened up google maps and started googling.
Flipping it to satellite mode, I kept finding myself frustrated trying to gauge the distance! Google maps is great for "Point A" -> "Point B" mapping. But for this use case, suddenly you find yourself putting useless destinations everywhere, adding a bunch of waypoints, clicking and clicking without any real results.
So I started searching for a map pedometer online, where I found mappedometer, which is okay, but the interface looks straight out of the 90's and the map doesn't work too well. Plus, it's littered with ads that make it super hard to use!
Being the frontend engineer I am, familiar with working with maps from my past jobs, I decided to take it upon myself to craft my OWN maps pedometer app.
So, I found an api for map loading and went at it! The maps are built on the maplibre free tier using the maptiler api.
The map loading code:
import { onCleanup, onMount } from "solid-js";
import type { RoutePoint, RouteView } from "../../domain/route";
import {
ensureRouteLayers,
updateRouteView,
} from "../../infrastructure/map/routeLayers";
import type { GeocodeResult } from "../../infrastructure/geocoding/geocodeLocation";
import { MapStyle } from "../../domain/preferences";
import type { Observable, Subscription } from "rxjs";
import "maplibre-gl/dist/maplibre-gl.css";
interface RouteMapProps {
readonly routeView$: Observable<RouteView>;
readonly mapStyle$: Observable<MapStyle>;
readonly searchResult$: Observable<GeocodeResult>;
readonly location$: Observable<RoutePoint>;
readonly onMapClick: (point: RoutePoint) => void;
readonly mapTilerKey?: string;
readonly streetsStyleUrl: string;
readonly satelliteStyleUrl?: string | null;
}
const withMapTilerKey = (url: string, key?: string) =>
key && url.startsWith("https://api.maptiler.com/") && !url.includes("key=")
? `${url}${url.includes("?") ? "&" : "?"}key=${key}`
: url;
export const RouteMap = (props: RouteMapProps) => {
let container!: HTMLDivElement;
let map: import("maplibre-gl").Map | null = null;
let latestView: RouteView | null = null;
let currentStyle: MapStyle | null = null;
const applyView = () => {
if (map && latestView) {
updateRouteView(map, latestView);
}
};
onMount(async () => {
const maplibre = await import("maplibre-gl");
const MapCtor = maplibre.Map;
container.style.position = "absolute";
container.style.inset = "0";
map = new MapCtor({
container,
style: props.streetsStyleUrl,
center: [-73.9851, 40.758],
zoom: 14,
transformRequest: (url: string) => ({
url: withMapTilerKey(url, props.mapTilerKey),
}),
});
map.on("load", () => {
ensureRouteLayers(map!);
applyView();
});
map.on("click", (event: import("maplibre-gl").MapMouseEvent) => {
const { lng, lat } = event.lngLat;
props.onMapClick({ lng, lat });
});
onCleanup(() => {
map?.remove();
map = null;
});
});
const routeSub: Subscription = props.routeView$.subscribe(
(view: RouteView) => {
latestView = view;
applyView();
},
);
onCleanup(() => routeSub.unsubscribe());
const styleSub: Subscription = props.mapStyle$.subscribe(
(style: MapStyle) => {
if (!map) {
currentStyle = style;
return;
}
if (style === currentStyle) return;
currentStyle = style;
const targetStyle =
style === MapStyle.Satellite && props.satelliteStyleUrl
? props.satelliteStyleUrl
: props.streetsStyleUrl;
if (!targetStyle) return;
const center = map.getCenter();
const zoom = map.getZoom();
const bearing = map.getBearing();
const pitch = map.getPitch();
map.setStyle(targetStyle, { diff: false });
map.once("style.load", () => {
ensureRouteLayers(map!);
map!.jumpTo({ center, zoom, bearing, pitch });
applyView();
});
},
);
onCleanup(() => styleSub.unsubscribe());
const searchSub: Subscription = props.searchResult$.subscribe(
(result: GeocodeResult) => {
if (!map || !result.location) return;
map.flyTo({
center: [result.location.lng, result.location.lat],
zoom: 14,
});
},
);
onCleanup(() => searchSub.unsubscribe());
const locationSub: Subscription = props.location$.subscribe(
(point: RoutePoint) => {
if (!map) return;
map.flyTo({
center: [point.lng, point.lat],
zoom: Math.max(map.getZoom(), 14),
});
},
);
onCleanup(() => locationSub.unsubscribe());
return <div ref={container} />;
};I architected this app to use RxJS to manage all the data points as they change. This is clever, since it plays so nicely with SolidJS.
To get the actual map locations I'm utilizing the ORS foot-walking api. Notice how I'm streaming them directly onto the map via Rx bindings
import { Observable, catchError, defer, map, of, switchMap } from "rxjs";
import { z } from "zod";
import type { RoutePoint } from "../../domain/route";
import { AppErrorKind, createAppError } from "../../shared/appError";
import { trackRequest } from "../../shared/requestActivity";
const ORS_DIRECTIONS_PATH = "v2/directions/foot-walking/geojson";
const joinUrl = (baseUrl: string, path: string): string => {
const sanitizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
const sanitizedPath = path.startsWith("/") ? path.slice(1) : path;
return `${sanitizedBase}${sanitizedPath}`;
};
const RouteGeometrySchema = z.object({
features: z
.array(
z.object({
geometry: z.object({
coordinates: z.array(z.tuple([z.number(), z.number()])),
}),
}),
)
.min(1),
});
export interface OrsFootWalkingDeps {
readonly baseUrl: string;
readonly apiKey?: string;
readonly fetchImpl?: typeof fetch;
readonly requiresAuth?: boolean;
}
export interface OrsFootWalkingInput {
readonly start: RoutePoint;
readonly end: RoutePoint;
}
export type OrsFootWalking = (
input$: Observable<OrsFootWalkingInput>,
) => Observable<RoutePoint[]>;
const toRoutePoints = (coordinates: [number, number][]): RoutePoint[] =>
coordinates.map(([lng, lat]) => ({ lng, lat }));
export const createOrsFootWalking =
({
baseUrl,
apiKey,
fetchImpl = fetch,
requiresAuth = true,
}: OrsFootWalkingDeps): OrsFootWalking =>
(input$) => {
const endpoint = joinUrl(baseUrl, ORS_DIRECTIONS_PATH);
const authRequired = requiresAuth;
const authToken = authRequired ? apiKey : undefined;
return input$.pipe(
switchMap(({ start, end }: OrsFootWalkingInput) => {
if (authRequired && !authToken) {
return of<RoutePoint[]>([start, end]);
}
const headers: Record<string, string> = {
"Content-Type": "application/json; charset=utf-8",
};
if (authToken) {
headers.Authorization = authToken;
}
return trackRequest(() =>
defer(() =>
fetchImpl(endpoint, {
method: "POST",
headers,
body: JSON.stringify({
coordinates: [
[start.lng, start.lat],
[end.lng, end.lat],
],
instructions: false,
elevation: false,
preference: "recommended",
geometry_simplify: false,
}),
}),
).pipe(
switchMap((response: Response) => {
if (!response.ok) {
return of<RoutePoint[]>([start, end]);
}
return defer(() => response.json()).pipe(
map((json: unknown) => {
const parsed = RouteGeometrySchema.parse(json);
const [feature] = parsed.features;
if (!feature) {
throw createAppError(
AppErrorKind.Domain,
"ORS response was missing geometry",
json,
);
}
return toRoutePoints(feature.geometry.coordinates);
}),
);
}),
catchError((error: unknown) => {
console.warn(
"ORS routing failed, falling back to direct segment",
error,
);
return of<RoutePoint[]>([start, end]);
}),
),
);
}),
map((points: RoutePoint[]) => {
if (points.length < 2) {
throw createAppError(
AppErrorKind.Domain,
"ORS returned insufficient route points",
);
}
return points;
}),
);
};
My fully interactive mapping app leverages a few free api's to accomplish the autocomplete search for your area, an interactive map kit from Map Box, and an awesome calorie counter. It runs fully client-side in SolidJS and doesn't store any data. No ads, and supreme usability. The only API code I needed was a simple proxy in Micronaut to help me mask my api keys.
It's incredibly accurate and gives you the motivation you might need to go out there and get some exercise!
Give it a shot and tell me we what you think! And, be safe and have fun going for a walk or jog in your neighborhood!