Your Leaflet code on the 3D globe
npm i @ortho-earth/leaflet @ortho-earth/globe
@ortho-earth/leaflet gives you an L namespace shaped like Leaflet 1.9's. Its maps, tile layers, markers, popups, vector layers and GeoJSON
draw through MapLibre GL: either on the Ortho Earth globe (@ortho-earth/globe/maplibre) or on the real maplibre-gl.
Code written for Leaflet keeps its shape; only the import changes. Leaflet's stylesheet is not needed.
// on the globe
import L from "@ortho-earth/leaflet/globe";
const map = L.map("map").setView([35.6812, 139.7671], 15);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap" }).addTo(map);
L.marker([35.6812, 139.7671]).addTo(map).bindPopup("<b>Tokyo Station</b>").openPopup();
L.geoJSON(parks, { style: f => ({ fillColor: f.properties.kind === "park" ? "green" : "gray" }) }).addTo(map);
@ortho-earth/leaflet/globe also has Leaflet's named exports (map, tileLayer, marker, …), so a bundler alias ports a whole app without touching its imports:
// vite.config.js
export default { resolve: { alias: { leaflet: "@ortho-earth/leaflet/globe" } } };
To keep a flat map, give it maplibre-gl instead: createLeaflet(maplibregl, { style }) from @ortho-earth/leaflet, and installGlobal(L) for scripts and plugins that read window.L.
| Leaflet | On the globe |
|---|---|
L.map and its options, setView, flyTo, fitBounds, panTo, point conversions, handlers (map.dragging.disable() …) | the camera. Zoom is shifted by one (Leaflet zoom z = MapLibre zoom z − 1, 256 px vs 512 px tiles). fitBounds uses Leaflet's own arithmetic, so a port lands on the same zoom. Rotation and tilt stay off unless you ask for them (rotate: true) |
Map events (click, move, zoomend, popupopen …) | MapLibre events with Leaflet's event object (latlng, containerPoint, layerPoint). A path's mouse event reaches its groups and then the map, as in Leaflet |
L.tileLayer ({s}, {r}, tms, your own {keys}, maxNativeZoom, bounds, opacity), L.tileLayer.wms, L.GridLayer with createTile | raster sources; a GridLayer's canvas or <img> tiles are drawn through a protocol |
L.marker, L.icon, L.divIcon, popups and tooltips (bindPopup, bindTooltip, permanent, sticky, direction) | MapLibre markers and popups, placed where Leaflet places them. The default icon is Leaflet's blue pin as SVG, no image files to serve |
L.polyline, L.polygon, L.rectangle, L.circle (metres), L.circleMarker (pixels), setStyle, toGeoJSON | one GeoJSON source per pane, like Leaflet's one renderer per pane, so 10,000 features cost one source |
L.geoJSON (style, pointToLayer, onEachFeature, filter), L.layerGroup, L.featureGroup | one layer per feature, as in Leaflet |
L.imageOverlay, L.videoOverlay | image and video sources |
L.control.zoom, attribution, scale, layers, L.Control.extend; panes (createPane, zIndex) | controls in MapLibre's corners; panes become the order of the layers |
L.Class, L.Evented, L.Util, L.DomUtil, L.DomEvent, L.latLng, L.latLngBounds, L.CRS … | plain values and helpers, so plugins built on L.Class.extend load |
L.CRS.Simple, EPSG:4326 maps): the globe is drawn in WGS84, and a flat MapLibre map in Web Mercator.path.getElement() returns null (a path is a MapLibre feature), and plugins that draw into Leaflet's panes or renderers, such as marker clustering or Leaflet.draw, do not draw.GridLayer tiles, detectRetina, and the zoomanim event.
Anything the namespace does not translate is reported once in the console as [lfshim] unsupported: …; it never throws.
L.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 Leaflet calls above against the real globe (npm run verify:ui -- t-leaflet in packages/globe).
Unlike MapLibre and CesiumJS, there is no gallery of Leaflet's own examples yet, so the tables on this page are the list of what is covered.
Coming from other libraries? See From Google Maps, From MapLibre GL JS and From CesiumJS.