Public
Edited
May 20, 2023
Importers
2 stars
Insert cell
Insert cell
Insert cell
{
let container = html`<div/>`;
yield container;
create_map(container, map_options );

}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
create_map = (container, options) => {
// update container size on resize/fullscreen event
let container_resize = () => {
// map size is defined by its parent element.
let width = container.parentElement.offsetWidth;
let height = (document.fullscreenElement ||
document.mozFullScreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement)
? container.parentElement.offsetHeight
: (width / 1.6);
container.style.width = options.width || width + 'px';
container.style.height = options.height || height + 'px';
};
window.onresize = () => { container_resize(); };
container_resize();

// supported layer types
let layer_types = {
WMTS: L.tileLayer,
WMS: L.tileLayer.wms,
GEOJSON: L.geoJSON,
VECTOR: L.vectorGrid.slicer,
PROTOBUF: L.vectorGrid.protobuf
};
function create_layer(layer, default_options) {
// create layer instance
var l = layer_types[layer.type](
layer.url,
{ ...default_options, ...layer.options }
);
// add event listener
for (const hndl of (layer.on || [])) {
l.on(hndl.event, hndl.handler);
}
return l;
};
function create_layers(layers, default_options) {
var result = {};
layers.forEach(layer => {
if (Array.isArray(layer)) {
var layer_name = undefined;
var subs = layer.map(l => {
layer_name = layer_name || l.name;
return create_layer(l, default_options);
});
result[layer_name] = L.layerGroup(subs);
}
else {
result[layer.name] = create_layer(layer, default_options);
}
});
return result;
}

// map content
let map = L.map(container, { ...options.map });

// base map layers
let bases = create_layers(options.layer.base);

// overlay map layers
let overlays = create_layers(options.layer.overlay, { opacity: 0.45, rendererFactory: L.canvas.tile });

// TODO: use generic solution (e.g. vectorGrid)??
// overlay: user data layers
options.layer.extra.forEach(def_layer => {
// create a top-level overlay layer for each data set
let user_layer_group = L.featureGroup.showHideOnZoom();
overlays[def_layer.name] = user_layer_group;
// data of the layer
const data = def_layer.filter
? def_layer.data.filter(def_layer.filter)
: def_layer.data;
const def_sub_layers = def_layer.subLayers ? def_layer.subLayers : []; // sub-layer definition
// split data to represent each sub-layer
const data_sub_layers = data.reduce(
(data_groups, elm) => {
// determine if elm belong to a sub-layer with its predicate
const matched = def_sub_layers.some((def_sub_layer, sub_layer_idx) => {
if (def_sub_layer.predicate(elm)) {
data_groups[sub_layer_idx].push(elm);
return true;
}
return false;
});
// collect rest elemnts in a default sub-layer
if (!matched) {
data_groups[data_groups.length - 1].push(elm);
}
return data_groups;
},
Array(def_sub_layers.length + 1) /* init data splitting result */
.fill()
.map(x => [])
);
// transform data from each sub-layer
data_sub_layers.forEach((data_sub_layer, sub_layer_idx) => {
const def_sub_layer = def_sub_layers[sub_layer_idx];
// create a sub-layer group with possible options
let options = L.Util.setOptions({}, def_layer.options);
if (def_sub_layer)
options = L.Util.setOptions(options, def_sub_layer.options);
let sub_layer_group = L.layerGroup([], options);
// Array.map function for the sub-layer
const map_sub_layer = def_sub_layer ? def_sub_layer.map : def_layer.map;
data_sub_layer.forEach(elm => {
// transform data to layers and add them to the sub-layer group
sub_layer_group.addLayer(map_sub_layer(elm));
});
// add to the sub-layer group to the top-level group
sub_layer_group.addTo(user_layer_group);
});
}); // overlay user data layers

// layer control
L.control.layers(bases, overlays).addTo(map);

// XXX: QnD UI which can be imporved....
// use `leaflet-geoman` only to add markers
// use `leaflet-popup-modifier` to edit popup content and remove markers.

// search control
/*
if (options.control.search) {
let search_control = new L.Control.Search({
url: 'https://nominatim.openstreetmap.org/search?format=json&q={s}',
jsonpParam: 'json_callback',
propertyName: 'display_name',
propertyLoc: ['lat', 'lon'],
marker: L.circleMarker([0, 0], { radius: 30 }).bindPopup(
L.popup({ removable: true, nametag: 'search result' })
),
autoCollapse: false, // set false, otherwise the search input text will not be availble to use
firstTipSubmit: true,
minLength: 2
}).on("search:locationfound", function(e) {
e.target.options.marker.getPopup().setContent(e.text);
e.target.collapse(); // instead of autoCollapse
});
map.addControl(search_control);
}
*/
// editing layer
/*
if (options.control.edit) {
// leaflet-geoman
let edit_layer = L.layerGroup().addTo(map);
map.pm.setGlobalOptions({ layerGroup: edit_layer });
map.pm.addControls({
position: 'topleft',
cutPolygon: false,
dragMode: false,
drawCircle: false,
drawRectangle: false,
drawPolygon: false,
drawPolyline: false,
drawCircleMarker: false,
editMode: false,
removalMode: false
});
// leaflet-geoman creates markers
map.on('pm:create', e => {
e.layer.bindPopup(
L.popup({ removable: true, editable: true }).setContent('')
);
mutable map_edit_layer = edit_layer; // expose edited layer
});
// leaflet-popup-modifier edits and removes markers
document.addEventListener("saveMarker", e => {
mutable map_edit_layer = edit_layer; // expose edited layer
});
document.addEventListener("removeMarker", e => {
e.detail.marker.removeFrom(edit_layer);
mutable map_edit_layer = edit_layer; // expose edited layer
});
}
*/
// scale
if (options.control.scale) L.control.scale({ maxWidth: 250 }).addTo(map);

// distance ruler

/*
if (options.control.ruler)
L.control.ruler({ position: "topleft" }).addTo(map);
*/
// show enabled base layers or the first base layer by default
var all_base_layers = options.layer.base.flat();
var any_enable_base = all_base_layers.filter(layer => layer.enable);
if (any_enable_base.length)
any_enable_base.forEach(l => { map.addLayer(bases[l.name]); });
else if (all_base_layers.length)
map.addLayer(bases[all_base_layers[0].name]);

// show enabled overlay layers
options.layer.overlay.forEach(layer => {
if (layer.enable)
map.addLayer(overlays[layer.name]);
});
// show enabled extra overlay layers
options.layer.extra.forEach(layer => {
if (layer.enable)
map.addLayer(overlays[layer.name]);
});

return map;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
map_overlay_layers = [
{
type: 'WMTS',
name: 'Slope (Low Res.)',
url:
'http://wmts.openslopemap.org/wmts/OSloOVERLAY_LR_Alps_16/{z}/{x}/{y}.png',
options: {
attribution:
'&copy; <a href="https://www.openslopemap.org/projekt/lizenzen/" target="_blank">OpenSlopeMap</a>, <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">CC-BY-SA</a>',
maxNativeZoom: 14
}
},
{
type: 'WMTS',
name: 'Slope (High Res.)',
url:
'http://wmts.openslopemap.org/wmts/OSloOVERLAY_UHR_AlpsEast_16/{z}/{x}/{y}.png',
options: {
attribution:
'&copy; <a href="https://www.openslopemap.org/projekt/lizenzen/" target="_blank">OpenSlopeMap</a>, <a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">CC-BY-SA</a>',
maxNativeZoom: 10
}
},
{
type: 'WMTS',
name: 'Hiking Trails',
url: 'https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png',
options: {
attribution:
'&copy; <a href="http://www.waymarkedtrails.org" target="_blank">waymarkedtrails.org</a>, <a href="https://creativecommons.org/licenses/by-sa/3.0/de/deed.de" target="_blank">CC BY-SA 3.0 DE</a>',
opacity: 0.65
}
},
{
type: 'WMTS',
name: 'Cycling Trails',
url: 'https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png',
options: {
attribution:
'&copy; <a href="http://www.waymarkedtrails.org" target="_blank">waymarkedtrails.org</a>, <a href="https://creativecommons.org/licenses/by-sa/3.0/de/deed.de" target="_blank">CC BY-SA 3.0 DE</a>',
opacity: 0.65
}
}
]
Insert cell
Insert cell
map_data_layers = [
{
name: 'Alpine Huts' /* layer name */,
data: data_alps_alpine_huts.elements /* data array */,
filter: my_fn_filter_alpine_huts /* Array.filter to filter data */,
map: my_fn_map_alpine_huts /* default Array.map applied on filtered data */,
options: { from_zoom: 11 } /* default options for L.Layer */,
/* sub layers definition */
subLayers: [
{
predicate: my_fn_pred_alpine_huts_from_club /* predicate to select filtered data */,
map: my_fn_map_alpine_huts_from_club /* optional Array.map to override default one */,
options: { from_zoom: 7 } /* options to override default ones */
},
{
predicate: my_fn_pred_alpine_huts_has_operator,
map: my_fn_map_alpine_huts_has_operator,
options: { from_zoom: 10 }
}
]
},
{
name: 'Alpine Hiking',
data: my_data_alpine_hiking.features,
map: my_fn_map_alpine_hiking,
options: { from_zoom: 6 }
},
{
name: 'Climbing',
data: my_data_climbing_bouldering.features,
map: my_fn_map_climbing_bouldering,
options: { from_zoom: 6 }
},
{
name: 'Travel Distance',
data: poi_transport,
map: my_fn_map_poi_transport,
options: { from_zoom: 6 }
}
]
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
L = {
// leaflet
var L = await require("leaflet@1.8.0");
await load_style_link('leaflet@1.8.0/dist/leaflet.css');

// plugins

// FIXME
/*
await require("leaflet-search@2.9.8");
await require('leaflet-search@2.9.8/dist/leaflet-search.src.js');
await load_style_link('leaflet-search@2.9.8/dist/leaflet-search.src.css');
*/
// FIXME
/*
await require("@geoman-io/leaflet-geoman-free@2.9.0").catch(() => L.PM);
await load_style_link(
'@geoman-io/leaflet-geoman-free@2.9.0/dist/leaflet-geoman.css'
);
*/

// FIXME
/*
{
// leaflet-popup-modifier
const response = await fetch(
"https://raw.githubusercontent.com/easz/leaflet-popup-modifier/master/popupMod.js"
);
const text = await response.text();
(1, eval)(`{${text}}`);

await load_style_text(
'https://raw.githubusercontent.com/easz/leaflet-popup-modifier/master/popupMod.css'
);
}
*/
// FIXME
/*
await require("https://cdn.jsdelivr.net/gh/gokertanrisever/leaflet-ruler@1.0.0/src/leaflet-ruler.js");
await load_style_link(
'https://cdn.jsdelivr.net/gh/gokertanrisever/leaflet-ruler@1.0.0/src/leaflet-ruler.css'
);
add_style_text(`.leaflet-ruler { height: 30px; width: 30px; }
.leaflet-ruler-clicked { height: 30px; width: 30px; }
`);
*/
// leaflet-fullscreen
leaflet_plugin_fullscreen_1_0_2(L);
load_style_link('leaflet-fullscreen@1.0.2/dist/leaflet.fullscreen.css');

// show/hide based on zoom level
leaflet_plugin_showHideOnZoom(L);

// leaflet.sync@0.2.4
leaflet_plugin_sync(L);

// leaflet.vectorgrid@1.3.0
leaflet_plugin_vectorgrid(L);
return L;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// or re-calculate poi_transport again
/*
poi_transport = find_poi_nearby(
[...my_data_alpine_hiking.features, ...my_data_climbing_bouldering.features],
build_index(data_alps_public_transport),
25
)
*/
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// or re-calculate poi_transport_travel_time_ms again
/*
calculate_shortest_travel_time_ms(
travel_from,
poi_transport,
[hafas_oebb, hafas_db],
[
departure_time_tomorrow,
departure_time_winter,
departure_time_spring,
departure_time_summer
]
)
*/
Insert cell
Insert cell
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