create_map = (container, options) => {
let container_resize = () => {
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();
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) {
var l = layer_types[layer.type](
layer.url,
{ ...default_options, ...layer.options }
);
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;
}