data overlays
This commit is contained in:
parent
60de7857d6
commit
7904a95b80
22 changed files with 1230 additions and 219 deletions
|
@ -1058,7 +1058,7 @@ main:not([data-view=route-result]) #route-summary {
|
|||
font-size: 20px;
|
||||
}
|
||||
|
||||
.leaflet-bar, .leaflet-touch .leaflet-bar, .leaflet-control-key {
|
||||
.leaflet-bar, .leaflet-touch .leaflet-bar, .leaflet-control-key, .leaflet-control-overlays {
|
||||
overflow: hidden;
|
||||
background-color: var(--color-control-background);
|
||||
border-radius: var(--border-radius-leaflet-control);
|
||||
|
@ -1676,10 +1676,10 @@ blink {
|
|||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.leaflet-control-key {
|
||||
.leaflet-control-key, .leaflet-control-overlays {
|
||||
background-clip: padding-box;
|
||||
|
||||
&.leaflet-control-key-expanded > .collapsed-toggle {
|
||||
&.leaflet-control-key-expanded > .collapsed-toggle, &.leaflet-control-overlays-expanded > .collapsed-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -1699,7 +1699,6 @@ blink {
|
|||
|
||||
&::before {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
content: 'legend_toggle';
|
||||
font-size: 26px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
@ -1736,23 +1735,36 @@ blink {
|
|||
}
|
||||
}
|
||||
|
||||
&.leaflet-control-key-expanded > .pin-toggle {
|
||||
&.leaflet-control-key-expanded > .pin-toggle, &.leaflet-control-overlays-expanded > .pin-toggle {
|
||||
display: block;
|
||||
.leaflet-touch & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
> .content {
|
||||
display: none;
|
||||
padding: 1rem 3rem 1rem 1rem;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 2rem 1fr;
|
||||
padding: 1rem 4rem 1rem 1rem;
|
||||
|
||||
.leaflet-touch & {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.leaflet-control-key-expanded > .content {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.leaflet-control-key {
|
||||
> .collapsed-toggle::before {
|
||||
content: 'legend_toggle';
|
||||
}
|
||||
|
||||
> .content {
|
||||
gap: 1rem;
|
||||
grid-template-columns: 2rem 1fr;
|
||||
|
||||
> .key {
|
||||
display: grid;
|
||||
|
@ -1774,6 +1786,59 @@ blink {
|
|||
}
|
||||
}
|
||||
|
||||
.leaflet-control-overlays {
|
||||
> .collapsed-toggle::before {
|
||||
content: 'stacks';
|
||||
}
|
||||
|
||||
> .content {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.overlay-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
content: 'arrow_right';
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
margin-left: 3ch;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-group.expanded h4::before {
|
||||
content: 'arrow_drop_down';
|
||||
}
|
||||
|
||||
.overlay-group:not(.expanded) label {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.leaflet-control-overlays-expanded > .content {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-top.leaflet-right {
|
||||
z-index: 2000;
|
||||
}
|
||||
|
@ -1798,4 +1863,43 @@ blink {
|
|||
.leaflet-top.leaflet-right {
|
||||
margin-top: var(--control-container-minus-size);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-point-icon {
|
||||
> span {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-family: 'Material Symbols Outlined';
|
||||
}
|
||||
}
|
||||
|
||||
.data-overlay-popup {
|
||||
.leaflet-popup-content {
|
||||
margin: 0;
|
||||
|
||||
> h4, a {
|
||||
margin: 8px 12px 4px;
|
||||
}
|
||||
|
||||
> table {
|
||||
width: calc(100% + 2px);
|
||||
margin: 4px -2px;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
&:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1449,6 +1449,7 @@ c3nav = {
|
|||
c3nav._routeLayers = {};
|
||||
c3nav._routeLayerBounds = {};
|
||||
c3nav._userLocationLayers = {};
|
||||
c3nav._overlayLayers = {};
|
||||
c3nav._firstRouteLevel = null;
|
||||
c3nav._labelLayer = L.LayerGroup.collision({margin: 5}).addTo(c3nav.map);
|
||||
for (i = c3nav.levels.length - 1; i >= 0; i--) {
|
||||
|
@ -1458,6 +1459,7 @@ c3nav = {
|
|||
c3nav._locationLayers[level[0]] = L.layerGroup().addTo(layerGroup);
|
||||
c3nav._routeLayers[level[0]] = L.layerGroup().addTo(layerGroup);
|
||||
c3nav._userLocationLayers[level[0]] = L.layerGroup().addTo(layerGroup);
|
||||
c3nav._overlayLayers[level[0]] = L.layerGroup().addTo(layerGroup);
|
||||
}
|
||||
c3nav._levelControl.finalize();
|
||||
c3nav._levelControl.setLevel(c3nav.initial_level);
|
||||
|
@ -1480,6 +1482,8 @@ c3nav = {
|
|||
position: 'bottomright'
|
||||
}).addTo(c3nav.map);
|
||||
|
||||
c3nav._update_overlays();
|
||||
|
||||
c3nav.map.on('click', c3nav._click_anywhere);
|
||||
|
||||
c3nav.schedule_fetch_updates();
|
||||
|
@ -1855,12 +1859,34 @@ c3nav = {
|
|||
_set_user_data: function (data) {
|
||||
c3nav_api.authenticate();
|
||||
c3nav.user_data = data;
|
||||
c3nav._update_overlays();
|
||||
var $user = $('header #user');
|
||||
$user.find('span').text(data.title);
|
||||
$user.find('small').text(data.subtitle || '');
|
||||
$('.position-buttons').toggle(data.has_positions);
|
||||
if (window.mobileclient) mobileclient.setUserData(JSON.stringify(data));
|
||||
},
|
||||
_current_overlays_key: null,
|
||||
_update_overlays: function () {
|
||||
if (!c3nav.map) return;
|
||||
|
||||
const key = c3nav.user_data.overlays.map(o => o.id).join(',');
|
||||
if (key === c3nav._current_overlays_key) return;
|
||||
c3nav._current_overlays_key = key;
|
||||
|
||||
const control = new OverlayControl({levels: c3nav._overlayLayers});
|
||||
for (const overlay of c3nav.user_data.overlays) {
|
||||
control.addOverlay(new DataOverlay(overlay));
|
||||
}
|
||||
|
||||
if (c3nav._overlayControl) {
|
||||
c3nav.map.removeControl(c3nav._overlayControl);
|
||||
}
|
||||
|
||||
if (c3nav.user_data.overlays.length > 0) {
|
||||
c3nav._overlayControl = control.addTo(c3nav.map);
|
||||
}
|
||||
},
|
||||
|
||||
_hasLocationPermission: undefined,
|
||||
hasLocationPermission: function (nocache) {
|
||||
|
@ -2489,7 +2515,7 @@ KeyControl = L.Control.extend({
|
|||
this._pin = L.DomUtil.create('div', 'pin-toggle material-symbols', this._container);
|
||||
this._pin.classList.toggle('active', pinned);
|
||||
this._pin.innerText = 'push_pin';
|
||||
this._collapsed = L.DomUtil.create('a', 'collapsed-toggle leaflet-control-key-toggle', this._container);
|
||||
this._collapsed = L.DomUtil.create('a', 'collapsed-toggle', this._container);
|
||||
this._collapsed.href = '#';
|
||||
this._expanded = pinned;
|
||||
this._pinned = pinned;
|
||||
|
@ -2502,7 +2528,6 @@ KeyControl = L.Control.extend({
|
|||
}
|
||||
|
||||
|
||||
|
||||
if (L.Browser.touch) {
|
||||
this._pinned = false;
|
||||
console.log('installing touch handlers')
|
||||
|
@ -2588,6 +2613,162 @@ KeyControl = L.Control.extend({
|
|||
},
|
||||
});
|
||||
|
||||
OverlayControl = L.Control.extend({
|
||||
options: {position: 'topright', addClasses: '', levels: {}},
|
||||
_overlays: {},
|
||||
_groups: {},
|
||||
_initialActiveOverlays: null,
|
||||
_initialCollapsedGroups: null,
|
||||
|
||||
initialize: function ({levels, ...config}) {
|
||||
this.config = config;
|
||||
this._levels = levels;
|
||||
},
|
||||
|
||||
onAdd: function () {
|
||||
this._initialActiveOverlays = JSON.parse(localStorage.getItem('c3nav.overlays.active-overlays') ?? '[]');
|
||||
this._initialCollapsedGroups = JSON.parse(localStorage.getItem('c3nav.overlays.collapsed-groups') ?? '[]');
|
||||
const pinned = JSON.parse(localStorage.getItem('c3nav.overlays.pinned') ?? 'false');
|
||||
|
||||
this._container = L.DomUtil.create('div', 'leaflet-control-overlays ' + this.options.addClasses);
|
||||
this._container.classList.toggle('leaflet-control-overlays-expanded', pinned);
|
||||
this._content = L.DomUtil.create('div', 'content');
|
||||
const collapsed = L.DomUtil.create('div', 'collapsed-toggle');
|
||||
this._pin = L.DomUtil.create('div', 'pin-toggle material-symbols');
|
||||
this._pin.classList.toggle('active', pinned);
|
||||
this._pin.innerText = 'push_pin';
|
||||
this._container.append(this._pin, this._content, collapsed);
|
||||
this._expanded = pinned;
|
||||
this._pinned = pinned;
|
||||
|
||||
if (!L.Browser.android) {
|
||||
L.DomEvent.on(this._container, {
|
||||
mouseenter: this.expand,
|
||||
mouseleave: this.collapse
|
||||
}, this);
|
||||
}
|
||||
|
||||
if (!L.Browser.touch) {
|
||||
L.DomEvent.on(this._container, 'focus', this.expand, this);
|
||||
L.DomEvent.on(this._container, 'blur', this.collapse, this);
|
||||
}
|
||||
|
||||
for (const overlay of this._initialActiveOverlays) {
|
||||
if (overlay in this._overlays) {
|
||||
this._overlays[overlay].visible = true;
|
||||
this._overlays[overlay].enable(this._levels);
|
||||
}
|
||||
}
|
||||
|
||||
for (const group of this._initialCollapsedGroups) {
|
||||
if (group in this._groups) {
|
||||
this._groups[group].expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.render();
|
||||
|
||||
$(this._container).on('change', 'input[type=checkbox]', e => {
|
||||
this._overlays[e.target.dataset.id].visible = e.target.checked;
|
||||
this.updateOverlay(e.target.dataset.id);
|
||||
});
|
||||
$(this._container).on('click', 'div.pin-toggle', e => {
|
||||
this.togglePinned();
|
||||
});
|
||||
$(this._container).on('click', '.content h4', e => {
|
||||
this.toggleGroup(e.target.parentElement.dataset.group);
|
||||
});
|
||||
$(this._container).on('mousedown pointerdown wheel', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
return this._container;
|
||||
},
|
||||
|
||||
addOverlay: function (overlay) {
|
||||
this._overlays[overlay.id] = overlay;
|
||||
if (overlay.group in this._groups) {
|
||||
this._groups[overlay.group].overlays.push(overlay);
|
||||
} else {
|
||||
this._groups[overlay.group] = {
|
||||
expanded: this._initialCollapsedGroups === null || !this._initialCollapsedGroups.includes(overlay.group),
|
||||
overlays: [overlay],
|
||||
};
|
||||
}
|
||||
this.render();
|
||||
},
|
||||
|
||||
updateOverlay: function (id) {
|
||||
const overlay = this._overlays[id];
|
||||
if (overlay.visible) {
|
||||
overlay.enable(this._levels);
|
||||
} else {
|
||||
overlay.disable(this._levels);
|
||||
}
|
||||
const activeOverlays = Object.keys(this._overlays).filter(k => this._overlays[k].visible);
|
||||
localStorage.setItem('c3nav.overlays.active-overlays', JSON.stringify(activeOverlays));
|
||||
},
|
||||
|
||||
render: function () {
|
||||
if (!this._content) return;
|
||||
const groups = document.createDocumentFragment();
|
||||
for (const group in this._groups) {
|
||||
const group_container = document.createElement('div');
|
||||
group_container.classList.add('overlay-group');
|
||||
if (this._groups[group].expanded) {
|
||||
group_container.classList.add('expanded');
|
||||
}
|
||||
this._groups[group].el = group_container;
|
||||
group_container.dataset.group = group;
|
||||
const title = document.createElement('h4');
|
||||
title.innerText = group;
|
||||
group_container.append(title);
|
||||
for (const overlay of this._groups[group].overlays) {
|
||||
const label = document.createElement('label');
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.dataset.id = overlay.id;
|
||||
if (overlay.visible) {
|
||||
checkbox.checked = true;
|
||||
}
|
||||
label.append(checkbox, overlay.name);
|
||||
group_container.append(label);
|
||||
}
|
||||
groups.append(group_container);
|
||||
}
|
||||
this._content.replaceChildren(...groups.children);
|
||||
},
|
||||
|
||||
expand: function () {
|
||||
if (this._pinned) return;
|
||||
this._expanded = true;
|
||||
this._container.classList.add('leaflet-control-overlays-expanded');
|
||||
return this;
|
||||
},
|
||||
|
||||
collapse: function () {
|
||||
if (this._pinned) return;
|
||||
this._expanded = false;
|
||||
this._container.classList.remove('leaflet-control-overlays-expanded');
|
||||
return this;
|
||||
},
|
||||
|
||||
toggleGroup: function (name) {
|
||||
const group = this._groups[name];
|
||||
group.expanded = !group.expanded;
|
||||
group.el.classList.toggle('expanded', group.expanded);
|
||||
const collapsedGroups = Object.keys(this._groups).filter(k => !this._groups[k].expanded);
|
||||
localStorage.setItem('c3nav.overlays.collapsed-groups', JSON.stringify(collapsedGroups));
|
||||
},
|
||||
|
||||
togglePinned: function () {
|
||||
this._pinned = !this._pinned;
|
||||
if (this._pinned) {
|
||||
this._expanded = true;
|
||||
}
|
||||
this._pin.classList.toggle('active', this._pinned);
|
||||
localStorage.setItem('c3nav.overlays.pinned', JSON.stringify(this._pinned));
|
||||
},
|
||||
});
|
||||
|
||||
var SvgIcon = L.Icon.extend({
|
||||
options: {
|
||||
|
@ -2645,4 +2826,99 @@ var SvgIcon = L.Icon.extend({
|
|||
|
||||
return svgEl;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
class DataOverlay {
|
||||
levels = null;
|
||||
|
||||
constructor(options) {
|
||||
this.id = options.id;
|
||||
this.name = options.name;
|
||||
this.group = options.group ?? 'ungrouped';
|
||||
this.default_stroke_color = options.stroke_color;
|
||||
this.default_stroke_width = options.stroke_width;
|
||||
this.default_fill_color = options.fill_color;
|
||||
}
|
||||
|
||||
async create() {
|
||||
const features = await c3nav_api.get(`mapdata/overlays/${this.id}/`);
|
||||
|
||||
const levels = {};
|
||||
for (const feature of features) {
|
||||
const level_id = feature.level_id;
|
||||
if (!(level_id in levels)) {
|
||||
levels[level_id] = L.layerGroup([]);
|
||||
}
|
||||
const style = {
|
||||
'color': feature.stroke_color ?? this.default_stroke_color ?? 'var(--color-map-overlay)',
|
||||
'weight': feature.stroke_width ?? this.default_stroke_width ?? 1,
|
||||
'fillColor': feature.fill_color ?? this.default_fill_color ?? 'var(--color-map-overlay)',
|
||||
};
|
||||
const layer = L.geoJson(feature.geometry, {
|
||||
style,
|
||||
interactive: feature.interactive,
|
||||
pointToLayer: (geom, latlng) => {
|
||||
if (feature.point_icon !== null) {
|
||||
return L.marker(latlng, {
|
||||
title: feature.title,
|
||||
icon: L.divIcon({
|
||||
className: 'overlay-point-icon',
|
||||
html: `<span style="color: ${style.color}">${feature.point_icon}</span>`,
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12],
|
||||
})
|
||||
});
|
||||
} else {
|
||||
return L.circleMarker(latlng, {
|
||||
title: feature.title,
|
||||
...style
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if (feature.interactive) {
|
||||
layer.bindPopup(() => {
|
||||
let html = `<h4>${feature.title}</h4>`;
|
||||
if (feature.external_url != null) {
|
||||
html += `<a href="${feature.external_url}" target="_blank">open external link</a>`;
|
||||
}
|
||||
if (feature.extra_data != null) {
|
||||
html += '<table>';
|
||||
for (const key in feature.extra_data) {
|
||||
html += `<tr><th>${key}</th><td>${feature.extra_data[key]}</td></tr>`;
|
||||
}
|
||||
|
||||
html += '</table>';
|
||||
}
|
||||
return html;
|
||||
}, {
|
||||
className: 'data-overlay-popup'
|
||||
});
|
||||
}
|
||||
levels[level_id].addLayer(layer);
|
||||
}
|
||||
|
||||
this.levels = levels;
|
||||
}
|
||||
|
||||
async enable(levels) {
|
||||
if (!this.levels) {
|
||||
await this.create();
|
||||
}
|
||||
for (const id in levels) {
|
||||
if (id in this.levels) {
|
||||
levels[id].addLayer(this.levels[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disable(levels) {
|
||||
for (const id in levels) {
|
||||
if (id in this.levels) {
|
||||
levels[id].removeLayer(this.levels[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue