Closed dahlalex closed 1 year ago
I thought about this before. Unfortunately, cesium doesn't have a Python API/package. It won't be easy for leafmap to support cesium. Suggestions are welcome.
Anywidget can potentially make CesiumJS work with Jupyter. Here is an example I just tried. It does not work yet. I am not a JavaScript expert and I am not sure how to make it work yet. @manzt might be able to shed some lights on this.
https://cesium.com/learn/cesiumjs-learn/cesiumjs-quickstart/
import anywidget
import traitlets
class MapWidget(anywidget.AnyWidget):
# Widget front-end JavaScript code
_esm = """
import * as Cesium from "https://cesium.com/downloads/cesiumjs/releases/1.103/Build/Cesium/Cesium.js";
export function render(view) {
// create a div element
const cesiumContainer = document.createElement("div");
cesiumContainer.style.height = "600px";
Cesium.Ion.defaultAccessToken = 'your_access_token';
// Initialize the Cesium Viewer in the HTML element with the `cesiumContainer` ID.
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain()
});
// Add Cesium OSM Buildings, a global 3D buildings layer.
const buildingTileset = viewer.scene.primitives.add(Cesium.createOsmBuildings());
// Fly the camera to San Francisco at the given longitude, latitude, and height.
viewer.camera.flyTo({
destination : Cesium.Cartesian3.fromDegrees(-122.4175, 37.655, 400),
orientation : {
heading : Cesium.Math.toRadians(0.0),
pitch : Cesium.Math.toRadians(-15.0),
}
});
view.el.appendChild(cesiumContainer);
}
"""
# make sure to include styles
_css = "https://cesium.com/downloads/cesiumjs/releases/1.103/Build/Cesium/Widgets/widgets.css"
m = MapWidget()
m
The issue is the JavaScript code is not an ESM, but rather UMD bundle that is intended to be loaded from a <script>
tag. It some JavaScript that when loaded has the side effect of mutating the global namespace by appending Cesium
. This is an old style of shipping JavaScript via a CDN that relies on convention over an official standard like ESM.
It would be nice for Cesium to ship ESM through their CDN since it is an official modern standard, and mutating the global namespace generally a poor practice when there is other JS on the page. However, there is a workaround to load this type of JS with a little JavaScript:
function loadScript(src) {
return new Promise((resolve, reject) => {
let script = Object.assign(document.createElement("script"), {
type: "text/javascript",
async: true,
src: src,
});
script.addEventListener("load", resolve);
script.addEventListener("error", reject);
document.body.appendChild(script);
});
};
The loadScript
function returns a Promise
object that resolves when the script is successfully loaded, or rejects when there is an error loading the script. This ensures that we "wait" until the Cesium
global is available before executing render
. A deeper explanation/more robust loadScript
function can be found in this post for a deeper explanation.
A full example,
import anywidget
import traitlets
class MapWidget(anywidget.AnyWidget):
# Widget front-end JavaScript code
_esm = """
function loadScript(src) {
return new Promise((resolve, reject) => {
let script = Object.assign(document.createElement("script"), {
type: "text/javascript",
async: true,
src: src,
});
script.addEventListener("load", resolve);
script.addEventListener("error", reject);
document.body.appendChild(script);
});
};
await loadScript("https://cesium.com/downloads/cesiumjs/releases/1.103/Build/Cesium/Cesium.js");
export function render(view) {
// create a div element
const cesiumContainer = document.createElement("div");
cesiumContainer.style.height = "600px";
Cesium.Ion.defaultAccessToken = 'your_access_token';
// Initialize the Cesium Viewer in the HTML element with the `cesiumContainer` ID.
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain()
});
// Add Cesium OSM Buildings, a global 3D buildings layer.
const buildingTileset = viewer.scene.primitives.add(Cesium.createOsmBuildings());
// Fly the camera to San Francisco at the given longitude, latitude, and height.
viewer.camera.flyTo({
destination : Cesium.Cartesian3.fromDegrees(-122.4175, 37.655, 400),
orientation : {
heading : Cesium.Math.toRadians(0.0),
pitch : Cesium.Math.toRadians(-15.0),
}
});
view.el.appendChild(cesiumContainer);
}
"""
# make sure to include styles
_css = "https://cesium.com/downloads/cesiumjs/releases/1.103/Build/Cesium/Widgets/widgets.css"
m = MapWidget()
m
The above now raises an exception for an invalid access token:
@manzt Thank you very much for providing the solution. This looks very promising. I signed up for a Cesium account and got an access token for using with the example. I then tried it on Colab, Jupyter Lab, Jupyter Notebook. All of them ran into errors like this:
Sorry, I don't have the time to look into this more deeply. It seems like some issue with Cesium JS looking for a global define
(typically from requirejs), which may or may not be present in the notebook environment. This issue stems from Cesium not shipping ESM, so there isn't really anything we can do on the anywidget side unfortunately.
Ok, I actually looked into this. I'm guessing the error you shared above is from Jupyter Notebooks (which uses requirejs and messes up this define
thing). However, the following works in JupyterLab:
import anywidget
import traitlets
class MapWidget(anywidget.AnyWidget):
# Widget front-end JavaScript code
_esm = """
function loadScript(src) {
return new Promise((resolve, reject) => {
let script = Object.assign(document.createElement("script"), {
type: "text/javascript",
async: true,
src: src,
});
script.addEventListener("load", resolve);
script.addEventListener("error", reject);
document.body.appendChild(script);
});
};
await loadScript("https://cesium.com/downloads/cesiumjs/releases/1.103/Build/Cesium/Cesium.js");
export function render(view) {
const div = document.createElement("div");
div.style.width = "500px";
div.style.height = "500px";
Cesium.Ion.defaultAccessToken = view.model.get("token");
const viewer = new Cesium.Viewer(div, {
terrainProvider: Cesium.createWorldTerrain()
});
const buildingTileset = viewer.scene.primitives.add(Cesium.createOsmBuildings());
viewer.camera.flyTo({
destination : Cesium.Cartesian3.fromDegrees(-122.4175, 37.655, 400),
orientation : {
heading : Cesium.Math.toRadians(0.0),
pitch : Cesium.Math.toRadians(-15.0),
}
});
view.el.appendChild(div);
}
"""
# make sure to include styles
_css = "https://cesium.com/downloads/cesiumjs/releases/1.103/Build/Cesium/Widgets/widgets.css"
token = traitlets.Unicode().tag(sync=True)
m = MapWidget(token="YOUR_TOKEN")
m
The errors you got before in Colab and JupyterLab arise from this line in the js:
const viewer = new Cesium.Viewer('cesiumContainer', {
where "esiumContainer" is an ID for a <div>
in the Cesium example. You haven't created that element, so Cesium throws and error. Remember that any time you use a JS library in the browser, there is typically some point where you need to connect the JS to a DOM node.
@manzt Amazing work! The ability to use non-ESM javascript library is very exciting. I will explore this and see how it can be integrated into leafmap.
@manzt Anywidget's hot module replacement functionality is mind-blowing! Thank you.
Another example using the maplibre library.
import anywidget
import traitlets
class MapWidget(anywidget.AnyWidget):
_esm = """
function loadScript(src) {
return new Promise((resolve, reject) => {
let script = Object.assign(document.createElement("script"), {
type: "text/javascript",
async: true,
src: src,
});
script.addEventListener("load", resolve);
script.addEventListener("error", reject);
document.body.appendChild(script);
});
};
await loadScript("https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js");
export function render(view) {
const div = document.createElement("div");
div.style.width = "100%";
div.style.height = "500px";
var map = new maplibregl.Map({
container: div,
style: 'https://demotiles.maplibre.org/style.json', // stylesheet location
center: [0, 20], // starting position [lng, lat]
zoom: 2 // starting zoom
});
view.el.appendChild(div);
}
"""
# make sure to include styles
_css = "https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css"
token = traitlets.Unicode().tag(sync=True)
m = MapWidget()
m
Another example using the mapbox library. Anywdiget has unlimited potential!
import anywidget
import traitlets
class MapWidget(anywidget.AnyWidget):
_esm = """
function loadScript(src) {
return new Promise((resolve, reject) => {
let script = Object.assign(document.createElement("script"), {
type: "text/javascript",
async: true,
src: src,
});
script.addEventListener("load", resolve);
script.addEventListener("error", reject);
document.body.appendChild(script);
});
};
await loadScript("https://api.mapbox.com/mapbox-gl-js/v2.13.0/mapbox-gl.js");
export function render(view) {
const div = document.createElement("div");
div.style.width = "100%";
div.style.height = "500px";
let token = view.model.get("token");
mapboxgl.accessToken = token
const map = new mapboxgl.Map({
container: div,
style: 'mapbox://styles/mapbox/streets-v12',
center: [-100, 40],
zoom: 2
});
view.el.appendChild(div);
}
"""
# make sure to include styles
_css = "https://api.mapbox.com/mapbox-gl-js/v2.13.0/mapbox-gl.css"
token = traitlets.Unicode().tag(sync=True)
token = 'YOUR-TOKEN'
m = MapWidget(token=token)
m
An example using the OpenLayers library.
import anywidget
import traitlets
class MapWidget(anywidget.AnyWidget):
_esm = """
function loadScript(src) {
return new Promise((resolve, reject) => {
let script = Object.assign(document.createElement("script"), {
type: "text/javascript",
async: true,
src: src,
});
script.addEventListener("load", resolve);
script.addEventListener("error", reject);
document.body.appendChild(script);
});
};
await loadScript("https://cdn.jsdelivr.net/npm/ol@v7.3.0/dist/ol.js");
export function render(view) {
const div = document.createElement("div");
div.style.width = "100%";
div.style.height = "500px";
var map = new ol.Map({
target: div,
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: ol.proj.fromLonLat([0, 20]),
zoom: 2
})
});
view.el.appendChild(div);
}
"""
# make sure to include styles
_css = "https://cdn.jsdelivr.net/npm/ol@v7.3.0/ol.css"
token = traitlets.Unicode().tag(sync=True)
m = MapWidget()
m
3D mapping functionality will be available through the mapwidget package. https://github.com/opengeos/mapwidget
A suggestion could be an integration with