@m1z23r/maps-core
v1.1.0
Published
Self-owned canvas map of Serbia — framework-agnostic engine
Readme
@m1z23r/maps-core
A self-owned canvas map of Serbia — a framework-agnostic engine drawn on
<canvas> from vector tiles (no MapLibre/Leaflet/Google). Zero runtime
dependencies; ships ESM + .d.ts.
npm i @m1z23r/maps-coreQuickstart
import { MapView } from '@m1z23r/maps-core';
const map = new MapView(document.getElementById('map')!, {
apiUrl: '/api/v1', // same-origin; or 'https://maps.dimitrije.dev/api/v1'
theme: 'dark',
});
map.on('ready', () => {
// A price badge pinned to a world point (EPSG:32634 meters).
map.pins.set([{ id: 1, x: 457396, y: 4962263, kind: 'badge', label: '€120' }]);
// Coarse privacy display: highlight a whole settlement, not an exact address.
void map.showLocation({ level: 'settlement', settlementId: 12345, label: 'Vračar' });
});
map.on('pinclick', ({ pin }) => console.log('clicked', pin.data));The container element sizes the map (it fills width/height: 100%); a
ResizeObserver keeps the canvas in sync.
Config
| Field | Default | Meaning |
|---|---|---|
| apiUrl | — (required) | base URL of the maps API, e.g. /api/v1 |
| apiKey | — | sent as X-Api-Key on every request (keyed tier) |
| theme | 'light' | 'light' or 'dark' |
| showRegions | false | region tint overlay |
| interactive | true | pan/zoom/click handling |
| styleOverrides | — | deep-partial overrides of the style tables |
Engine API (MapView)
on/off(event, cb)— see events below.setTheme(t),theme,setShowRegions(b),showRegions,setStyle(overrides)getCamera(),flyTo({ x, y, mpp }),fitBounds(bbox),resize()pins.set(pins)/pins.clear()— screen-space markers ('dot' | 'pin' | 'badge')overlays.set(overlays)/overlays.clear()— filled/stroked world-space geometryshowLocation(spec)/clearLocation()— privacy-level display (address pin, street highlight, settlement/municipality blob)setHighlight(geometry | null),placeTag(x, y, label?),clearTag()api— the configuredMapsApiclient (convenience)destroy()
Events
| Event | Payload |
|---|---|
| ready | void (once, after first paint) |
| click | { x, y } (world meters) |
| labelclick | { kind, name, wx, wy } — kind: street \| place \| settlement \| municipality |
| pinclick | { pin } |
| move | { x, y, mpp } |
| render | { mpp, level, featureCount, drawMs } |
Headless API client (MapsApi)
Usable without a map — autocomplete, geocode, geometry:
import { MapsApi, MapsApiError } from '@m1z23r/maps-core';
const api = new MapsApi({ apiUrl: '/api/v1', apiKey: 'optional' });
const hits = await api.municipalities('nis');
try {
const loc = await api.locate(457396, 4962263);
} catch (e) {
if (e instanceof MapsApiError && e.quotaExceeded) { /* free-tier 429 */ }
}API keys go in as apiKey → X-Api-Key. On the keyless free tier, exceeding the
daily quota throws MapsApiError with .quotaExceeded === true (HTTP 429).
Coordinates
Everything is EPSG:32634 (UTM zone 34N) world meters. Convert to/from WGS84 with the exported helpers:
import { wgs84ToWorld, worldToWgs84 } from '@m1z23r/maps-core';
const { x, y } = wgs84ToWorld(44.8125, 20.4612); // Belgrade → ~457396, ~4962263
const { lat, lon } = worldToWgs84(x, y);Theming
theme: 'dark' | 'light' selects an embedded palette; styleOverrides deep-merges
on top. setStyle(overrides) and setTheme(t) re-skin a live map.
Interaction
Pan with one finger / mouse drag, zoom with the wheel or a double-click/tap. On touch, two fingers pinch to zoom (and pan by their midpoint).
