@mappedin/static-maps
v6.31.0-beta.0
Published
A POC SDK that produces styled, top-down SVG renderings of a map floor from a [`MapData`](https://www.npmjs.com/package/@mappedin/mappedin-js) instance.
Readme
@mappedin/static-maps
A POC SDK that produces styled, top-down SVG renderings of a map floor from a
MapData instance.
Usage
Installation
With NPM:
npm install @mappedin/static-mapsWith Yarn:
yarn add @mappedin/static-mapsGetting Started
import { getMapData } from '@mappedin/mappedin-js';
import { StaticMaps } from '@mappedin/static-maps';
const mapData = await getMapData({ key, secret, mapId });
const staticMaps = new StaticMaps(mapData);
const svg = staticMaps.getSVG(mapData.getByType('floor')[0], {
cameraTransform: { pitch: 0, bearing: 0, zoomLevel: 16 },
width: 600,
height: 400,
});
document.getElementById('map')!.innerHTML = svg;The cameraTransform uses the same units as mapView.Camera:
pitch— degrees (0 = top-down).bearing— degrees.zoomLevel— mercator zoom level (0–24).mappedin-demo-mallfits at ~16.
extras/basic-app
@mappedin/static-maps/extras/basic-app ships a fully-wired, printed-map app
on top of the SDK. Instead of calling getSVG yourself, you describe the page
with a JSON config — two side-by-side floor columns (each [SVG | legend]),
an optional QR info card, per-category label rules, and a "you are here"
marker — and BasicStaticApp loads the map, runs the label pipeline, and
mounts the page for you. Call export() to trigger the high-res,
Illustrator-compatible SVG download from your own button.
mapData accepts either getMapData credentials (the app loads the venue for
you) or a ready MapData instance you already have. When you pass credentials,
the key / secret are reused for the optional popularity ranking call; a
pre-built MapData instance skips that call since no credentials are available.
This entry point pulls in the qrcode package (a runtime dependency of
@mappedin/static-maps) only when you import extras/basic-app. Core
@mappedin/static-maps consumers that never import the basic-app do not need
to use it.
import { getMapData } from '@mappedin/mappedin-js';
import { BasicStaticApp, cornersToPolygon } from '@mappedin/static-maps/extras/basic-app';
// Option A: pass credentials and let BasicStaticApp call getMapData for you.
// const mapData = { key, secret, mapId };
// Option B: reuse a MapData instance you already loaded.
const mapData = await getMapData({ key, secret, mapId });
const app = new BasicStaticApp({
mapData,
container: '#map', // HTMLElement or selector
config: {
// Exactly two columns; the first renders on the left.
columns: [
{ floorId: 'm_floor_1', showQRCard: true },
{
floorId: 'm_floor_2',
// Optionally crop a column to a sub-region and label it.
crop: {
name: 'Food Court',
bbox: [-79.39, 43.64, -79.38, 43.65], // [minLon, minLat, maxLon, maxLat]
},
// Group locations into labelled zones in the legend.
zones: [
{
name: 'East Wing',
polygon: cornersToPolygon([
[43.6487, -79.3899],
[43.6491, -79.3881],
]),
},
],
},
],
qrCode: {
url: 'https://app.mappedin.com/',
heading: 'Looking for information?',
body: 'Scan the QR code to open the live map.',
},
// Per-category rendering rules; unlisted categories render as numbered pills.
categoryRules: {
Washrooms: { kind: 'icon-on-map-only', iconSvg: '<svg viewBox="0 0 24 24">…</svg>', perSpace: true },
Parking: { kind: 'name-on-map-only' },
Walls: { kind: 'ignored' },
},
youAreHere: {
coordinate: { latitude: 43.6489, longitude: -79.389 },
floorId: 'm_floor_1',
bearing: 90,
},
},
});
await app.render();
// Wire export() to your own UI affordance (button, menu item, shortcut).
document.getElementById('download')!.addEventListener('click', () => app.export());Call app.destroy() to empty the container and release the cached export data.
Styling, camera & ranking
Beyond the layout, the config exposes three optional blocks for fine-tuning how each floor renders. All fields have sensible defaults, so add only what you need.
const config: BasicStaticAppConfig = {
// Schema version of this config. Stamp new configs so the SDK can migrate
// older payloads forward; omitting it is treated as version 1.
version: 1,
columns: [
/* … */
],
qrCode: { url: 'https://app.mappedin.com/' },
// Pin specific categories above/below the popularity-derived collision rank.
// 'low' | 'medium' | 'high' still collide; 'always-visible' never hides.
rankOverrides: {
Gates: 'high',
Washrooms: 'low',
},
// Camera applies to every column (cropped or not).
camera: {
pitch: 15, // degrees; 0 = top-down. Raise to tilt into a 2.5D view.
extrusionMultiplier: 0.5, // 1 = full MVF height, 0 = flat. Visible only at pitch > 0.
},
// Visual effects layered onto each floor map.
effects: {
// Soft drop shadow under each floor (offset + blur + colour).
mapShadow: { x: 0, y: 0, blur: 2 },
// The border drawn around the whole floor footprint.
mapOutline: { width: 1, color: '#777777' },
// The outlines drawn around each individual space polygon.
polygonOutlines: { color: '#777777', opacity: 0.8 },
},
};rankByPopularity(boolean, defaulttrue) — log-normalise label collision rank by location popularity. Setfalseto rank every label evenly and skip the analytics network call.rankOverrides(Record<string, BasicStaticAppRank>) — per-category rank overrides keyed by category name. Wins over the popularity rank.camera.pitch/camera.extrusionMultiplier— tilt and wall-height scale, applied to all columns. Extrusion is only visible whenpitch > 0.effects.mapShadow—x/yoffset,blur, andcolorof the floor's drop shadow. Omit for a default soft shadow;{ blur: 0 }(no offset) disables it.effects.mapOutline—width(0hides it) andcolorof the footprint border around the whole floor.effects.polygonOutlines—colorandopacityof the per-space outlines (defaults to a darkened version of each space's own fill).
React
BasicStaticApp manages its own DOM, so in React you give it a host <div>
ref on mount and let it own the page chrome, label pipeline, and SVG export. A
small wrapper component is all you need; use the onReady callback to grab the
app instance and wire up your own "download" affordance.
import { useEffect, useRef } from 'react';
import {
BasicStaticApp,
type BasicStaticAppConfig,
type BasicStaticAppOptions,
} from '@mappedin/static-maps/extras/basic-app';
interface BasicStaticAppViewProps extends Omit<BasicStaticAppOptions, 'container'> {
/** Fires once `render()` resolves, with the live app instance. */
onReady?: (app: BasicStaticApp) => void;
}
function BasicStaticAppView({ mapData, config, onReady }: BasicStaticAppViewProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
const app = new BasicStaticApp({ mapData, container: ref.current, config });
void app.render().then(() => onReady?.(app));
return () => app.destroy();
}, [mapData, config, onReady]);
return <div ref={ref} style={{ width: '100%', height: '100%' }} />;
}
function App() {
const appRef = useRef<BasicStaticApp | null>(null);
// `mapData` accepts credentials (shown here) or a ready `MapData` instance.
const mapData = { key, secret, mapId };
const config: BasicStaticAppConfig = {
columns: [{ floorId: 'm_floor_1', showQRCard: true }, { floorId: 'm_floor_2' }],
qrCode: { url: 'https://app.mappedin.com/' },
};
return (
<>
<button type="button" onClick={() => appRef.current?.export()}>
Download SVG
</button>
<BasicStaticAppView mapData={mapData} config={config} onReady={app => (appRef.current = app)} />
</>
);
}Keep config (and mapData) referentially stable — e.g. memoize them or hold
them in state — so the effect only re-renders the map when the inputs actually
change.
