Public
Edited
Oct 25, 2023
Fork of Leaflet POC
Insert cell
Insert cell
// worldCopyJump
Insert cell
{
const layers = [];
map.map.eachLayer(l => layers.push(l));
return layers;
}
Insert cell
m = new L.Map(DOM.element("div"));
Insert cell
map.map.getCenter()
Insert cell
map.map.getZoom()
Insert cell
map.map.flyTo({lat: -0.6364206182756988, lng: -49.91245580521718}, 7.228212509167584);
Insert cell
topoRef
Insert cell
mutable topoRef = false;
Insert cell
crs.scale(map.map.getZoom())
Insert cell
layers.filter(l => l.markDirty)
Insert cell
layers = {
const layers = [];
map.map.eachLayer(l => layers.push(l));
return layers;
}
Insert cell
topoRef._sourceArcs.flat().filter(c => c[2] >= 1)
Insert cell
{
const path = [...document.querySelectorAll("path.leaflet-interactive")].filter(p => p.getAttribute("d").length > 150)[0]
return path.getAttribute("d")
}
Insert cell
map = {
const height = width * mapSize.height / mapSize.width;
const container = html`<div style="height:${height}px;">`;
yield container;

// const corner1 = L.latLng(-90, -180)
// const corner2 = L.latLng(90, 180)
// const maxBounds = L.latLngBounds(corner1, corner2)

const renderer = new L.SVG({ padding: 0 });
const map = L.map(container, {
zoomAnimation: false,
zoomSnap: 0,
topoMove: true,
// maxBounds: [[-90, -Infinity], [90, Infinity]],
worldCopyJump: true,
renderer
});
const topo = new LeafletTopology(countries, map);
const topoLayer = topo.createLayer(countries.objects.countries);
topoLayer.addTo(map);
mutable topoRef = topo;
// const topoLayer = new TopoJsonLayer(countries, countries.objects.countries, { padding: 1 });
// topoLayer.addTo(map);
// const bounds = L.latLngBounds(L.latLng(-90, -180), L.latLng(90, 180));
// map.fitBounds(bounds);

map.setMaxBounds([[-90, -360], [90, 360]])
function fitWorld() {
const zoomY = map.getBoundsZoom(L.latLngBounds(L.latLng(-90, 0), L.latLng(90, 0)));
const zoomX = map.getBoundsZoom(L.latLngBounds(L.latLng(0, -180), L.latLng(0, 180)));
map.setMinZoom(Math.max(zoomX, zoomY));
}
fitWorld();
map.fitBounds([[-90, -360], [90, 360]]);

invalidation.then(() => map.remove());
container.map = map;
yield container;
}
Insert cell
countries = {
const geojson = await fetch("https://cdn.jsdelivr.net/gh/martynafford/natural-earth-geojson/50m/cultural/ne_50m_admin_0_countries_lakes.json").then(r => r.json());
for (let c of geojson.features) {
delete c.properties;
}
let topology = topojson.topology({ countries: geojson });
return topology; // topology.presimplify
}
Insert cell
[
L.Map.prototype.options.crs.wrapLng,
L.Map.prototype.options.crs.wrapLat
]
Insert cell
L.Map.prototype.options.crs.latLngToPoint(L.latLng(0,180), -8)
Insert cell
L.Map.prototype.options.crs.zoom(1)
Insert cell
// countries = await fetch("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-10m.json").then(r => r.json());
Insert cell
L.CRS.Earth
Insert cell
turf.featureCollection()
Insert cell
sp = d3.scalePow()
.exponent(2)
.domain([0, 1, 3])
.range([0.5, 0.5, 6]);
Insert cell
sp(3)
Insert cell
map.getrend
Insert cell
rtLayer = undefined;
/*
{
const scale = d3.scalePow()
.exponent(2)
.domain([0, 1, 5])
.range([1, 1, 10]);
return L.realtime(turf.featureCollection([]), {
noClip: true,
smoothFactor: 1,
weight: 1,
fnWeight(lineWidth, renderer) {
return scale(renderer._zoom) * lineWidth;
}
});
}
*/
Insert cell
Insert cell
Insert cell
clippedSVG = new ClippedSVG()
Insert cell
Insert cell
Insert cell
Insert cell
mapSize = ({ width: 2048, height: 904 });
Insert cell
Insert cell
topoJson
Insert cell
Insert cell
geoJson
Insert cell
topoJson
Insert cell
contigTopology =
return makeContiguous(st2, st2.objects.traced);
}
Insert cell
Bbox.toString()
Insert cell
Insert cell
import { smoothTopology, makeContiguous, sampleTopology as topoJson, topojson, Bbox, sampleGeoJson as geoJson } from "8c9dbd3e4bfdb2e9"
Insert cell
Flatbush = require('flatbush@4.2.0/flatbush.js');
Insert cell
topojson.feature(topoJson, topoJson.objects.traced)
Insert cell
topoJson.objects
Insert cell
leafletBase = await require('leaflet@1.2.0');
Insert cell
topoJson.objects.traced.geometries[0]
Insert cell
topojson.feature(topoJson, topoJson.objects.traced.geometries[0])
Insert cell
_.intersection(Object.keys(L.Polygon.prototype), Object.keys(L.Polyline.prototype))
Insert cell
// pa = new PolygonArc(topoJson, topoJson.objects.traced.geometries[0]);
Insert cell
topoJson.objects.traced
Insert cell
topoJson.objects.traced.geometries[0]
Insert cell
Insert cell
d3 = require("d3", "d3-geo", "d3-geo-projection", "d3-regression");
Insert cell
simple = topojson.presimplify(countries);
Insert cell
L = require('leaflet@1.9.4/dist/leaflet-src.js')
Insert cell
html`<link href='${resolve('leaflet@1.9.4/dist/leaflet.css')}' rel='stylesheet' />`
Insert cell
{
const allGeometries = Object.values(simple.objects).flatMap(o => o.geometries);
return _.groupBy(allGeometries.map(g => g.arcs.flat().flat()).flat().map(id => (id < 0 ? ~id : id)));
// .flatMap(o => o.arcs.flat().flat()))).filter(i => i.length > 1)
}
Insert cell
Insert cell
polyPathOverrides = ({
getTopology() {
return this._topology;
},
setTopology(topology) {
this._setTopology(topology);
return this.redraw();
},
getGeometry() {
return this._geometry;
},
setGeometry(geometry) {
this._setGeometry({ type: geometry?.type, arcs: geometry?.arcs });
return this.redraw();
},
setLatLngs(latlngs) {
throw "Can't call setLatLngs on a topology path.";
},
markDirty() {
this._latlngs = false;
this.redraw();
},
isEmpty() {
return !this._geometry?.arcs?.length;
},
_setTopology(topology) {
this._topology = topology;
this.markDirty();
},
_setGeometry(geometry) {
this._geometry = geometry;
this.markDirty();
},
_simplifyPoints() {
return;
},
_clipPoints() {
this._parts = this._rings;
},
});
Insert cell
PolylineArc = {

const levelsDeep = {
LineString: 0,
MultiLineString: 1,
Polygon: 1,
MultiPolygon: 2
};
return L.Polyline.extend({
...polyPathOverrides,
options: {
smoothFactor: 0,
noClip: true
},
initialize(topology, geometry, options) {
L.Util.setOptions(this, options);
this._setTopology(topology);
this._setGeometry({ type: geometry?.type, arcs: geometry?.arcs, properties: geometry?.properties ?? {} });
this.__latlngs = false;
Object.defineProperty(this, "_latlngs", {
get() {
if (this.__latlngs === false) {
if (!this._topology || !this._geometry) return false;
const feature = topojson.feature(this._topology, this._geometry);
const latlngs = L.GeoJSON.coordsToLatLngs(feature.geometry.coordinates, levelsDeep[geometry.type]);
this._setLatLngs(latlngs);
}
return this.__latlngs;
},
set(value) {
this.__latlngs = value;
this.redraw();
},
enumerable: true,
configurable: true
})
},
/*
_updatePath: function() {
this.__latlngs = false;
}
*/
});
}
Insert cell
PolygonArc = L.Polygon.extend({
...polyPathOverrides,
options: {
smoothFactor: 0,
noClip: true,
fill: true
},
initialize() {
PolylineArc.prototype.initialize.call(this, ...arguments);
},
/*
_updatePath: function () {
this.__latlngs = false;
L.Polygon.prototype._updatePath.call(this);
},
*/
});
Insert cell
Math.sqrt(simple.arcs.length)
Insert cell
{
const allScores = simple.arcs.flat().map(c => c[2]).filter(c => c > 0 && isFinite(c));
allScores.sort((a,b) => a-b);
const index = Math.floor(allScores.length - simple.arcs.length / 3);
return allScores[index];
// return { asl: allScores.length, al: simple.arcs.length, index };
return allScores.toReversed();
}
Insert cell
allScores = simple.arcs.flat().map(c => c[2]).filter(c => c > 0 && isFinite(c));
Insert cell
stdev = d3.deviation(allScores)
Insert cell
d3.mean(allScores) + 2 * stdev
Insert cell
lSimple._basePxArea
Insert cell
lSimple = new LeafletTopology(simple, map.map)
Insert cell
LeafletTopology = {
function coordsEqual(a, b) {
if (a === b) return true;
return (a.length == b.length && a[0] === b[0] && a[1] === b[1]);
}
function arcsEqual(a, b) {
if (a === b) return true;
if (!a || !b) return false;
// This isn't a heuristic, it's just how topojson works
return (a.length == b.length && coordsEqual(a[0], b[0]) && coordsEqual(a[1], b[1]));
}
function getBasePxArea(crs, latLngBounds) {
const pointNw = crs.latLngToPoint(latLngBounds.getNorthWest(), 1);
const pointSe = crs.latLngToPoint(latLngBounds.getSouthEast(), 1);
const bounds = L.bounds(pointNw, pointSe);
const size = bounds.getSize();
return size.x * size.x + size.y * size.y;
}
class LeafletTopology {
constructor(topology, map) {
this._map = map;
this._basePxArea = undefined;
this._ringPxAreas = new Map();
this._arcLayerIds = new Map();
this._loadTopology(topology);
const fnUpdateArcs = this._updateArcs.bind(this);
const startUpdating = (function() {
if (this._layerUpdateInterval) return;
this._layerUpdateInterval = setInterval(fnUpdateArcs, 100);
}).bind(this);
const stopUpdating = (function() {
clearInterval(this._layerUpdateInterval);
this._layerUpdateInterval = undefined;
this._updateArcs();
}).bind(this);
map.on("movestart", startUpdating);
map.on("moveend", stopUpdating);
map.once("unload", function() {
this._map.off("movestart", startUpdating);
this._map.off("moveend", stopUpdating);
this._map = undefined;
stopUpdating();
}, this);
}
createLayer(geometry, options) {
if (!geometry) return null;
if (geometry.type == "GeometryCollection") {
const layers = geometry.geometries.map(g => this.createLayer(g, options)).filter(g => g);
return new L.FeatureGroup(layers);
}
let layer;
switch (geometry.type) {
case 'LineString':
case 'MultiLineString':
layer = new PolylineArc(this, geometry, options);
break;
case 'Polygon':
case 'MultiPolygon':
layer = new PolygonArc(this, geometry, options);
break;
default:
throw new Error('Invalid/unsupported GeoJSON object.');
}
const stamp = L.Util.stamp(layer);
const arcIds = geometry.arcs.flat().flat().map(i => (i < 0) ? ~i : i);
arcIds.forEach(arcId => this._arcLayerIds.get(arcId).add(stamp));
return layer;
}
_updateArcs() {
const map = this._map;
if (!map) return;
const bounds = map.getBounds();
const visiblePxArea = getBasePxArea(this._getCrs(), bounds);
const pctVisible = visiblePxArea / this._basePxArea;
const nw = bounds.getNorthWest();
const se = bounds.getSouthEast();
const bbox = Bbox.fromPoints([[nw.lng, nw.lat], [se.lng, se.lat]]);
const visibleArcIds = new Set(this._index.search(...bbox));
// TODO: options.smoothFactor
const smoothFactor = 1;
const tolerance = Math.min(this._lowResFloorPx, smoothFactor * pctVisible);
const dirtyArcIds = new Set();
this._lowResArcs.forEach((arc, i) => {
if (visibleArcIds.has(i)) {
arc = this._sourceArcs[i].filter(c => c[2] >= tolerance).map(c => [c[0], c[1]]);
}
if (!arcsEqual(arc, this.arcs[i])) dirtyArcIds.add(i);
this.arcs[i] = arc;
});
const stampsToUpdate = new Set();
for (let arcId of dirtyArcIds) {
for (let stamp of this._arcLayerIds.get(arcId)) {
stampsToUpdate.add(stamp);
}
}
console.log(`Redrawing ${stampsToUpdate.size} objects @ tolerance ${smoothFactor / pctVisible}; ${this.arcs.flat().length}/${this._sourceArcs.flat().length} points in topology.`);
this._map.eachLayer(l => {
if (l.markDirty && stampsToUpdate.has(L.Util.stamp(l))) {
l.markDirty();
}
});
}
_getCrs() {
return this._map?.options.crs;
}
_loadTopology(topology) {
this.type = topology.type;
this.objects = topology.objects;
// Unquantize and create _sourceArcs
const transformCoord = topojson.transform(topology.transform);
const sourceArcs = this._sourceArcs = topology.arcs.map(arc => {
return arc.map(c => transformCoord(c));
});
// Build index
const index = this._index = new Flatbush(sourceArcs.length);
sourceArcs.forEach(arc => index.add(...Bbox.fromPoints(arc)));
index.finish();
// Presimplify the topology
this.presimplifyTopology();
// Build _lowResArcs and arcs
this._lowResArcs = sourceArcs.map(a => a.filter(c => c[2] >= this._lowResFloorPx).map(c => [c[0], c[1]]));
this.arcs = [...this._lowResArcs];
// Initialize this._arcLayerIds
for (let arcId = 0; arcId < sourceArcs.length; arcId++) {
if (this._arcLayerIds.has(arcId)) continue;
this._arcLayerIds.set(arcId, new Set());
}

console.log(`Topology loaded. ${sourceArcs.length} arcs, ${sourceArcs.flat().length} points.`);
}

presimplifyTopology() {
const sourceArcs = this._sourceArcs;
// Presimplify a temporary copy of this topology, with lat/lng converted to screen coords at zoom level 1
const crs = this._getCrs();
const presimplified = topojson.presimplify({
type: "Topology",
arcs: sourceArcs.map(arc => {
return arc.map(([lng,lat,value]) => {
const { x, y } = crs.latLngToPoint(L.latLng(lat, lng), 1);
return [x, y];
});
})
});
// Calculate this._basePxArea, the area (in pixels) of the topology at zoom level 1
const [n, e, s, w] = Bbox.fromPoints(presimplified.arcs.flat());
this._basePxArea = getBasePxArea(crs, L.latLngBounds(L.latLng(n, w), L.latLng(s, e)));

// Update sourceArcs to include the new presimplification scores, expressed as a fraction of this._basePxArea
sourceArcs.forEach((arc, i) => {
arc.forEach((coord, j) => {
if (coord[2] == Infinity) return;
coord[2] = presimplified.arcs[i][j][2];
});
});

const allScores = sourceArcs.flat().map(c => c[2]).filter(c => c > 0 && isFinite(c));
allScores.sort((a,b) => a-b);
const lowResFloorIndex = Math.floor(allScores.length - simple.arcs.length / 3);
this._lowResFloorPx = allScores[lowResFloorIndex];
}
}
return LeafletTopology;
}
Insert cell
turf = await require('@turf/turf@6.5.0/turf.min.js')
Insert cell
svg`<svg viewBox="0 0 100 100">
<defs>
<clipPath id="myClip">
<circle id="circle" cx="40" cy="35" r="35" />
</clipPath>
</defs>
<g transform="scale(1 0.5)">>
<use clip-path="url(#myClip)" href="#circle" fill="red" stroke="blue" stroke-width="10" />
</g>
</svg>`
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