Your Google Maps JavaScript API code on the 3D globe
npm i @ortho-earth/gmaps @ortho-earth/globe
@ortho-earth/gmaps gives you a google.maps namespace. Its Map, Marker, InfoWindow, shapes and Data
draw through MapLibre GL: either on the Ortho Earth globe (@ortho-earth/globe/maplibre) or on the real maplibre-gl.
Code written for Google Maps keeps its shape; only the import changes. The map itself needs no Google API key and sends nothing to Google.
// on the globe
import google from "@ortho-earth/gmaps/globe"; // { maps }
const map = new google.maps.Map(document.getElementById("map"), { center: { lat: 35.6812, lng: 139.7671 }, zoom: 15, tilt: 45 });
const marker = new google.maps.Marker({ map, position: { lat: 35.6812, lng: 139.7671 }, title: "Tokyo Station" });
const info = new google.maps.InfoWindow({ content: "<b>Tokyo Station</b>" });
marker.addListener("click", () => info.open({ map, anchor: marker }));
map.data.loadGeoJson("/parks.geojson");
map.data.setStyle(f => ({ fillColor: f.getProperty("kind") === "park" ? "green" : "gray" }));
Code that loads the API with @googlemaps/js-api-loader can take the Loader from @ortho-earth/gmaps/globe instead, and google.maps.importLibrary("maps" | "marker" | "places" | …) works too.
To keep a flat map, give createGoogleMaps(maplibregl, { styles }) a MapLibre style per map type; installGlobal(maps) sets window.google.maps for code that reads the global.
| Google Maps | On the globe |
|---|---|
Map options and methods (center, zoom, heading, tilt, restriction, moveCamera, panTo, fitBounds, controls[ControlPosition] …) | the camera. Zoom is shifted by one (Google zoom z = MapLibre zoom z − 1, 256 px vs 512 px tiles). mapTypeId and mapId pick a MapLibre style |
Map events (click, idle, bounds_changed, zoom_changed, dragend …) | MapLibre events with Google's event object (latLng, pixel, stop()). A click on a marker or a clickable shape is not also a map click, as in Google Maps |
Marker (icon as URL, Icon or Symbol, label, draggable), marker.AdvancedMarkerElement, marker.PinElement | MapLibre markers with Google's red pin, your image, or the SVG symbol |
InfoWindow (content, headerContent, pixelOffset, open({ map, anchor })) | a MapLibre popup above the marker, following it when it moves |
Polyline, Polygon (holes), Circle, Rectangle, MVCArray paths, geodesic, zIndex | GeoJSON line and fill layers; editing the path redraws it, and geodesic follows the great circle |
Data (addGeoJson, loadGeoJson, setStyle as an object or a function, overrideStyle, feature events) | one GeoJSON source; each feature's style is evaluated into its properties |
LatLng, LatLngBounds, MVCObject, MVCArray, event.*, geometry.spherical, geometry.encoding, geometry.poly | plain values; the geometry library is computed on a sphere of radius 6,378,137 m, as Google does |
The services that live on Google's servers need your own key: createGlobeMaps({ apiKey, language, region }), maps.setApiKey(key), or new Loader({ apiKey }).
The key is sent only to *.googleapis.com, and Google's terms and billing apply to these calls. Without a key the methods answer REQUEST_DENIED and the map keeps working.
| Google Maps | Relayed to |
|---|---|
Geocoder.geocode | Geocoding API |
DirectionsService.route, DirectionsRenderer, DistanceMatrixService | Routes API (the renderer draws the route and lettered markers on the map, and the steps in its panel) |
ElevationService | Elevation API |
places.PlacesService, places.AutocompleteService, places.Autocomplete | Places API (New) |
StreetViewService.getPanorama, StreetViewPanorama | Street View metadata and Static APIs: a still image that turns when dragged. Google does not publish its panorama tiles outside its own API, so there is no smooth 360° view |
styles: [{ featureType, … }]) and cloud-based styles: give MapLibre styles by map type or mapId instead.Polyline.icons, marker animations, overlayMapTypes, MVCObject.bindTo, and the map type, Street View and rotate controls.
Anything the namespace does not translate is reported once in the console as [gmshim] unsupported: …; it never throws.
google.maps.unsupportedReport() lists them, so you can check a port just by running it. For anything else, map.getMapLibre() returns the map underneath,
and on the globe that map also has everything in the engine: terrain, 3D Tiles, the shared clock and the night side.
The test suite runs the calls above against the real globe (npm run verify:ui -- t-gmaps in packages/globe), and every service against a stand-in for Google's servers.
Unlike MapLibre and CesiumJS, there is no gallery of Google's own samples yet, so the tables on this page are the list of what is covered.
Coming from other libraries? See From Leaflet, From MapLibre GL JS and From CesiumJS.