Migrating from Google Maps to Wemap
This guide is for teams that already ship a Google Maps JavaScript API integration on the web and want to move it to Wemap. It maps the Google APIs you use today onto their Wemap equivalents, shows the two integration paths, and flags the places where the two platforms differ in kind rather than in naming.
Migrating a native app from the Google Maps SDK for Android or iOS is a different exercise — start from the Android, iOS or mobile webview documentation instead.
The one thing that changes everything
Google Maps gives you a blank canvas: you create a Map, then push your data onto
it at runtime — markers, info windows, polylines — usually from your own backend on
every page load.
Wemap gives you a livemap: a map that already knows its content. Places (pinpoints), their grouping (lists), their styling, the indoor floors, the search behaviour and the UI are configured once — in Wemap Pro or through the Content API — and the map renders them for you.
Practically, that means a chunk of your current code disappears rather than gets translated. Fetching POIs, building marker icons, writing info-window HTML, clustering, "search this area" — those are map features on Wemap, not application code. The migration work is mostly deciding what to delete.
The second difference worth knowing up front: indoor is a first-class citizen.
Pinpoints, routes and markers can carry a floor level, and the map shows or hides them
according to the displayed floor. If your map is purely outdoor you can ignore levels
entirely; if you have buildings, this is the feature you were missing.
Google's single API key becomes two values: an emmid (the livemap id) and a
token (the owner's access token). Both come from
Wemap Pro. For the Content API you additionally create an
OAuth2 application to obtain a bearer token — see the REST API getting started.
Choose your target
Google's JS API is one product. Wemap's web offering is two, and the right choice depends on how much of the map's UI you wrote yourself.
| If your Google integration is… | Migrate to | Why |
|---|---|---|
| A map with markers, info windows and a sidebar you built to list/search places | Embed SDK | You get the search, POI cards, lists, routing UI and indoor switcher out of the box, and delete your own. |
| A map embedded inside a larger app UI where you own every pixel (your own panels, your own search field, your own marker rendering) | JavaScript SDK | @wemap/map renders the livemap and nothing else; you drive it from your own components. |
| Geocoding / Directions web services called from your backend | Services API | Server-side REST, no client SDK needed. |
A useful heuristic: start with the Embed SDK. It is the shortest migration by far, and it is the only one that replaces UI code rather than just map code. Move to the JS SDK only when you hit a customisation the Embed SDK doesn't expose.
Path A — Embed SDK
Creating the map
const map = new google.maps.Map(document.getElementById('map'), {
center: { lat: 48.8566, lng: 2.3522 },
zoom: 15,
});
<div id="map-container"></div>
<script src="https://livemap.getwemap.com/js/sdk.min.js"></script>
<script>
const container = document.getElementById('map-container');
const livemap = wemap.v1.createLivemap(container, { emmid: 1234, token: 'MY_TOKEN' });
livemap.waitForReady().then(() => {
livemap.centerTo({ latitude: 48.8566, longitude: 2.3522 }, 15, true);
});
</script>
Two things to internalise:
- No initial
center/zoomin code. The livemap's default viewport, style, zoom range and bounds come from its configuration. Override at runtime if you must, but prefer configuring it once in Wemap Pro. waitForReady()before anything else. It is the equivalent of Google'sidleevent, and calls made before it resolves are not guaranteed to land.
API mapping
| Google Maps | Embed SDK |
|---|---|
map.setCenter(latLng) | livemap.setCenter({ latitude, longitude }) |
map.setZoom(z) | livemap.setZoom(z) |
map.panTo() / combined center+zoom | livemap.centerTo(center, zoom, animate) or livemap.easeTo(options) |
map.fitBounds(bounds) | livemap.fitBounds([W, S, E, N], { padding, animate }) |
map.getCenter() / getZoom() | livemap.getCenter() / getZoom() — both return Promises |
map.setHeading() | livemap.setBearing(bearing, { duration }) |
new google.maps.Marker({...}) | livemap.setPinpoints(pinpoints, { type: 'add' }) — full POI objects with the built-in card UI |
marker.setMap(null) / rebuilding your marker set | livemap.setPinpoints(nextPinpoints, { type: 'replace' }) |
new google.maps.Polyline({ path }) | livemap.drawPolyline(coords, { color, width, opacity }) / removePolyline(id) |
DirectionsService + DirectionsRenderer | livemap.drawPolyline(coords, { useNetwork: true }), or navigateToPinpoint(id) / directionToPinpoint(id) for the full guidance UI |
MarkerClusterer (separate library) | Built in — configured on the livemap |
map.addListener('click', cb) | livemap.addEventListener('mapClick', cb) |
| Your own "search this area" UI | Built-in search, plus livemap.setFilters(filters, { type }) |
setPinpoints is the one to look at hardest: it is what most Google integrations
actually want. You hand it your places, and search, cards, clustering, filtering and
deep-links all work on them without further code.
Events
addEventListener / removeEventListener replace addListener and
google.maps.event.clearListeners. The names you will reach for first:
livemap.addEventListener('pinpointOpen', (e) => console.log(e.pinpoint.name));
livemap.addEventListener('mapClick', (e) => { /* … */ });
livemap.addEventListener('mapMoved', (e) => { /* Google: 'idle' */ });
livemap.addEventListener('contentUpdated', (e) => { /* visible content changed */ });
Also available: ready, mapLongClick, mapUpdate, pinpointClick,
pinpointClose, listOpen/listClose, eventOpen/eventClose,
indoorLevelChanged, floorChanged, guidingStarted/guidingUpdated/guidingStopped,
userLocationUpdated, fullscreenEnter/fullscreenExit, actionButtonClick,
userLogin/userLogout, permissionsDenied.
See the event listeners example.
Path B — JavaScript SDK
Use this when you need the map surface without the Wemap UI. @wemap/map is a thin
wrapper over maplibre-gl, so if you have ever used Mapbox GL
or MapLibre the API will feel familiar — and the raw instance stays reachable as
map.maplibre.
Creating the map
import { core } from '@wemap/core';
import { Coordinates } from '@wemap/geo';
import { WemapMap } from '@wemap/map';
import 'maplibre-gl/dist/maplibre-gl.css';
import '@wemap/map/style.css';
await core.init({ emmid: '31668', token: 'YOUR_TOKEN' });
const map = new WemapMap({ container: 'map' });
await map.whenReady();
map.setCenter(new Coordinates(48.8566, 2.3522));
core.init() has no Google equivalent — it fetches the livemap configuration that
every other package reads. It must run before you construct a WemapMap.
There is no <script>-loader callback and no libraries= parameter: install the
packages you need, or load the UMD bundles in dependency order. See
Installation.
API mapping
| Google Maps | JS SDK |
|---|---|
new google.maps.Map(el, opts) | new WemapMap({ container: 'map', ...opts }) |
google.maps.event.addListenerOnce(map, 'idle', …) | await map.whenReady() |
new google.maps.LatLng(lat, lng) | new Coordinates(lat, lng, alt?, level?) from @wemap/geo |
LatLngBounds | BoundingBox(northEast, southWest) from @wemap/geo |
map.setCenter() / setZoom() / getBounds() | map.setCenter() / setZoom() / getBounds() — synchronous, like Google |
map.panTo() | map.flyTo({ center, zoom, duration }) |
map.fitBounds(bounds) | map.fitBounds(boundingBox) |
new google.maps.Marker() | new DomMarkerLayer(map) then markers.add({ id, position, color | icon | element }) |
marker.setMap(null) | markers.remove(id) |
marker.addListener('click', cb) | markers.on('click', ({ id, position }) => …) |
MarkerClusterer | new DomMarkerLayer(map, { cluster: { enable: true } }) |
new google.maps.Polyline() | shapes.drawLineString(geometry, style) on a ShapeLayer |
new google.maps.Polygon() | shapes.drawPolygon(geometry, style) |
map.data.addGeoJson(json) | ShapeLayer with simplestyle-spec properties |
DirectionsService.route() | new Router().directions(origin, destination, 'WALK', options) |
DirectionsRenderer | new ItineraryLayer(map).set(itinerary, { fitBounds: true }) |
map.setOptions({ myLocationEnabled }) | new UserLocationLayer(map) fed from @wemap/positioning |
PlacesService.nearbySearch / your POI backend | map.search(query), map.filterPois(), map.setPoiHighlighted() |
| POI click on Google's own POIs | map.onPoiClick(({ pinpoint, coordinates }) => …) |
map.addListener('click', cb) | map.on('click', cb) — also layer-scoped: map.on('click', 'layer-id', cb) |
| (no equivalent) | map.setLevel() / LevelControl — indoor floors |
Routing
new google.maps.DirectionsService().route(
{ origin, destination, travelMode: google.maps.TravelMode.WALKING },
(result, status) => { if (status === 'OK') renderer.setDirections(result); }
);
import { Router } from '@wemap/routing';
import { ItineraryLayer } from '@wemap/map';
const itineraries = await router.directions(origin, destination, 'WALK', {
isWheelchair: true,
});
new ItineraryLayer(map).set(itineraries[0], { fitBounds: true });
Modes are WALK, BIKE, CAR, TRANSIT. Google's waypoints and
optimizeWaypoints exist under the same names. isWheelchair (avoid stairs and
escalators) has no Google equivalent, and is usually a reason teams migrate in the
first place. Full detail in the Routing guide.
Distance Matrix maps to POST /v4/directions-multiple on the
Routing API: itineraries to many destinations in one call, in
log(n) rather than one request per destination. Origin and destinations must sit
within the same custom map.
Gotchas
These are the ones that cost real debugging time.
Coordinate order. Google is { lat, lng }. Wemap's SDK inputs are
latitude-first — new Coordinates(lat, lng), { lat, lng }, or [lat, lng]. But
GeoJSON geometries passed to ShapeLayer, and anything you do directly on
map.maplibre (including passthrough map.on('click') events), use [lng, lat].
Mixing them up puts your data in the ocean, silently.
The container needs an explicit height. Same as Google: a bare <div> collapses
to 0px and the map renders blank.
Zoom is fractional. 16.5 is valid. Google's integer-ish zoom levels are not a
constraint here.
Getters are async in the Embed SDK, sync in the JS SDK. livemap.getCenter()
returns a Promise (the map lives in an iframe); map.getCenter() does not.
Layers are not destroyed with the map. DomMarkerLayer, ShapeLayer,
ItineraryLayer and UserLocationLayer need destroy(), or
map.remove({ layers: true }) to cascade. Forgetting this leaks on SPA route changes.
HTTPS is mandatory for positioning and VPS (or localhost in development) —
browsers block geolocation and camera access on insecure origins.
Bringing your data over
Your Google integration almost certainly fetches places from your backend and turns them into markers on every page load. On Wemap you have two ways to handle that, and they can be combined on the same map.
Option 1 — Managed content
Your places live in Wemap as pinpoints, grouped into lists, and the livemap renders them without any code from you. Get them in by:
- Wemap Pro — manually, or by bulk import (xlsx and other formats) for a one-off migration.
- Data sync — when your source of truth stays on your side, we set up a custom synchronisation between your data and the Wemap backend, so the map always reflects it. Talk to us about your format and refresh rate rather than writing the integration yourself. The same pinpoints are also reachable through the Content API if you prefer to push them.
This is the option that deletes the most code. Search, categories and tags, media, POI cards, clustering, filtering, deep-linking and indoor binding all come with it. Prefer it whenever your places are reasonably stable.
Option 2 — Dynamic injection
Your data stays in your backend and you push it to the map at runtime:
- Embed SDK —
livemap.setPinpoints(pinpoints, { type: 'add' \| 'replace' }). The injected places still get the built-in card and search UI. - JS SDK —
DomMarkerLayerfor plain pins you render and handle yourself.
Use it for anything that cannot be pre-loaded: per-user or per-session results, real-time positions (vehicles, availability), stock or pricing that changes by the minute, or content you are still prototyping.
Which one
| Your data is… | Use |
|---|---|
| Stable, shared by all users, editable by non-developers | Managed content |
| Owned by your CMS but slow-moving | Content API sync |
| Per-user, real-time, or computed on the fly | Dynamic injection |
Mapping your Google sources
| Your Google source | Move it to |
|---|---|
| A POI table in your database | Pinpoints via POST /v3.0/pinpoints — see the Pinpoints API |
| A My Maps / KML / GeoJSON export | Convert to pinpoints, then group with lists — or import the file in Wemap Pro |
| Place IDs from the Places API | Re-geocode addresses through /v3.0/geocoding/geocode; Google Place IDs are not portable |
| Marker icons and categories | Categories and tags on the pinpoint, styled once in Wemap Pro |
Pinpoint fields map closely to what you already store: name, latitude,
longitude, address, description (HTML), altitude, category, tags,
media_file. Keep your own identifiers on the pinpoint (tags work well) so you can
reconcile with your source system later.
Content API access is granted on request — contact us.
What does not map one-to-one
Being straight about this saves you a discovery call:
- Street View — the equivalent is Wemap Vision, our spatial computing offer: immersive on-site views with AR and VPS positioning on top. It answers "what does it look like there" differently from Street View rather than reproducing it — see Spatial computing.
- Places Autocomplete / Place Details — Wemap search covers your livemap content, not a global business directory. Global geocoding is available through the Geocoding service; a worldwide POI database is not.
- Traffic, transit and bicycling layers — not exposed as toggleable layers.
TRANSITrouting is supported where transit data is available for the deployment.
Conversely, these have no Google equivalent and are usually why you are here: indoor multi-level maps with automatic building selection, VPS positioning, wheelchair routing, and content management (pinpoints, lists, events, media) as a product rather than as your backend.
Migration checklist
- Get your
emmidandtokenfrom Wemap Pro. - Pick the path — Embed SDK unless you own the whole UI.
- Stand up an empty map and confirm it renders (container height,
waitForReady()/whenReady()). - Load your places as pinpoints (Content API) rather than pushing markers at runtime.
- Port your event listeners — this is usually where your remaining app logic lives.
- Delete: clustering library, info-window templates, POI fetching, search UI, "search this area", custom marker icon generation.
- Re-check coordinate order everywhere you touch GeoJSON or
map.maplibre. - Add levels if you have indoor content.