Published
Edited
Sep 24, 2020
1 fork
2 stars
Insert cell
Insert cell
{
// var html = `<div id="map"></div>`;
let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });
yield container;
var map = L.map(container).setView([40.7128, -74.0059], 4);

L.tileLayer('https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

L.marker([40.7128, -74.0059]).addTo(map)
.bindPopup('A Leaflet marker popup<br><b>(on top of Vega)</b>');
const leafletVega = new VegaLayer(spec, {
// Make sure the legend stays in place when moving (slower)
delayRepaint: false,
});

leafletVega.addTo(map);

return container
}
Insert cell
Insert cell
VegaLayer = (L.Layer ? L.Layer : L.Class).extend({
version: "0.8.6",
options: {
// FIXME: uses window.vega
// eslint-disable-next-line no-undef
vega: window && window.vega,

// If Vega spec creates controls (inputs), put them all into this container
bindingsContainer: undefined,

// Options to be passed to the Vega`s parse method
parseConfig: undefined,

// Options to be passed ot the Vega`s View constructor
viewConfig: undefined,

// If true, graph will be repainted only after the map has finished moving (faster)
delayRepaint: true,

// optional warning handler: (warning) => { ... }
onWarning: false,

// optional error handler: (err) => { ...; throw err; }
onError: false,
},

initialize(spec, options) {
L.Util.setOptions(this, options);

// expression parsing in Vega is global,
// ensure it hasn't been intialized before,
// and make sure calls to setMapView() only happen
// when the View instance was created by us

if (!vega.expressionFunction('setMapView')) {
vega.expressionFunction(
'setMapView',
function setMapView(...args) {
const view = this.context.dataflow;
const handler = view.Leaflet_setMapViewHandler;
if (!handler) throw new Error('setMapView() is not defined for this graph');
view.runAfter(() => handler(...args));
}
);
}
this._ignoreSignals = 0;
this.disableSignals = () => {
this._ignoreSignals++;
};
this.enableSignals = () => {
this._ignoreSignals--;
if (this._ignoreSignals < 0) {
this._ignoreSignals = 0;
throw new Error('Too many calls to enableSignals()');
}
};

// Inject signals into the spec
const vsi = new Vsi(options.onWarning);

vsi.overrideField(spec, 'padding', 0);
vsi.overrideField(spec, 'autosize', 'none');
vsi.addToList(spec, 'signals', ['zoom', 'latitude', 'longitude']);
vsi.addToList(spec, 'projections', [this.defaultProjection]);

this._spec = spec;
},

/**
* @param {L.Map} map
* @return {L.VegaLayer}
*/
addTo(map) {
map.addLayer(this);
return this;
},

onAdd(map) {
this._onAddAsync(map);
return this;
},

async _onAddAsync(map) {
this.disableSignals();

try {
this._map = map;
this._vegaContainer = L.DomUtil.create('div', 'leaflet-vega-container');
map._panes.overlayPane.appendChild(this._vegaContainer);

const { viewConfig } = this.options;

const dataflow = vega.parse(this._spec, this.options.parseConfig);

if (viewConfig && viewConfig.loader) {
const oldLoad = viewConfig.loader.load.bind(viewConfig.loader);
viewConfig.loader.load = (uri, opt) => oldLoad(uri, opt);
}
this._view = new vega.View(dataflow, viewConfig);

if (!viewConfig || viewConfig.logLevel === undefined) {
this._view.logLevel(vega.Warn);
}
if (this.options.onWarning) {
this._view.warn = this.options.onWarning;
}
if (this.options.onError) {
this._view.error = this.options.onError;
}

this._view
.padding({
left: 0, right: 0, top: 0, bottom: 0,
})
.initialize(this._vegaContainer, this.options.bindingsContainer)
.hover();

const onSignal = (sig, value) => this._onSignalChange(sig, value);

this._view
.addSignalListener('latitude', onSignal)
.addSignalListener('longitude', onSignal)
.addSignalListener('zoom', onSignal);

map.on(this.options.delayRepaint ? 'moveend' : 'move', () => this._resetAsync());
map.on('zoomend', () => this._resetAsync());

/**
* Given longitude/latitude/zoom or bounding box, position the map to those coordinates
* The function can be called in one of the following ways:
* setMapView(latitude, longitude)
* setMapView(latitude, longitude, zoom)
* setMapView([longitude, latitude])
* setMapView([longitude, latitude], zoom)
* setMapView([[lng1,lat1],[lng2,lat2]])
*/
this._view.Leaflet_setMapViewHandler = (...args) => {
function throwError() {
throw new Error('Unexpected setMapView() parameters. It could be called with a bounding box setMapView([[longitude1,latitude1],[longitude2,latitude2]]), or it could be the center point setMapView([longitude, latitude], optional_zoom), or it can be used as setMapView(latitude, longitude, optional_zoom)');
}

function checkArray(val) {
if (!Array.isArray(val) || val.length !== 2 ||
typeof val[0] !== 'number' || typeof val[1] !== 'number'
) {
throwError();
}
return val;
}

let lng;
let lat;
let zoom;
switch (args.length) {
default:
throwError();
break;
case 1: {
const arg = args[0];
if (Array.isArray(arg) && arg.length === 2 &&
Array.isArray(arg[0]) && Array.isArray(arg[1])
) {
// called with a bounding box, need to reverse order
const [lng1, lat1] = checkArray(arg[0]);
const [lng2, lat2] = checkArray(arg[1]);
map.fitBounds(L.latLngBounds(L.latLng(lat1, lng1), L.latLng(lat2, lng2)));
} else {
// called with a center point and no zoom
[lng, lat] = checkArray(arg);
}
break;
}
case 2:
if (Array.isArray(args[0])) {
[lng, lat] = checkArray(args[0]);
// eslint-disable-next-line prefer-destructuring
zoom = args[1];
} else {
[lat, lng] = args;
}
break;
case 3:
[lat, lng, zoom] = args;
break;
}

if (lat !== undefined && lng !== undefined) {
if (typeof lat !== 'number' || typeof lng !== 'number') {
throwError();
}
if (zoom === undefined) {
zoom = map.getZoom();
} else if (typeof zoom !== 'number') {
throwError();
}
map.setView({ lat, lng }, zoom);
}
};
await this._resetAsync(true);
} catch (err) {
this._reportError(err);
} finally {
this.enableSignals();
}
},

onRemove() {
if (this._view) {
this._view.finalize();
this._view = null;
}

// TODO: once Leaflet 0.7 is fully out of the picture, replace this with L.DomUtil.empty()
const el = this._vegaContainer;
while (el.firstChild) {
el.removeChild(el.firstChild);
}
},

_reportError(err) {
/* eslint-disable no-console */
if (this.options.onError) {
this.options.onError(err);
} else if (console && console.error) {
console.error(err);
}
/* eslint-enable */
},

_onSignalChange(sig, value) {
if (this._ignoreSignals) {
return;
}

try {
const map = this._map;
const center = map.getCenter();
let zoom = map.getZoom();

switch (sig) {
case 'latitude':
center.lat = value;
break;
case 'longitude':
center.lng = value;
break;
case 'zoom':
zoom = value;
break;
default:
return; // ignore
}

map.setView(center, zoom);
} catch (err) {
this._reportError(err);
}
},

async _resetAsync(force) {
if (!this._view) return;

this.disableSignals();
try {
const map = this._map;
const view = this._view;
const topLeft = map.containerPointToLayerPoint([0, 0]);
L.DomUtil.setPosition(this._vegaContainer, topLeft);

const size = map.getSize();
const center = map.getCenter();
const zoom = map.getZoom();

// Only send changed signals to Vega.
// Detect if any of the signals have changed before calling run()
const sendSignal = (sig, value) => {
if (view.signal(sig) !== value) {
view.signal(sig, value);
return 1;
}
return 0;
};

// update if any of the signal's values have changed
const changed =
sendSignal('width', size.x) +
sendSignal('height', size.y) +
sendSignal('latitude', center.lat) +
sendSignal('longitude', center.lng) +
sendSignal('zoom', zoom);

if (changed > 0 || force) {
await view.runAsync();
}
} catch (err) {
this._reportError(err);
} finally {
this.enableSignals();
}
},

defaultProjection: {
name: 'projection',
type: 'mercator',
scale: { signal: '256*pow(2,zoom)/2/PI' },
rotate: [{ signal: '-longitude' }, 0, 0],
center: [0, { signal: 'latitude' }],
translate: [{ signal: 'width/2' }, { signal: 'height/2' }],
fit: false,
},

});
Insert cell
Vsi = require('vega-spec-injector@0.0.2');
Insert cell
html`<link href='${resolve('leaflet@1.1.0/dist/leaflet.css')}' rel='stylesheet' />`
Insert cell
vega = require('vega@3.2.1');
Insert cell
L = require('leaflet@1.1.0');
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more