kopia lustrzana https://github.com/geodienst/lighthousemap
Render ALL the lighthouses!
rodzic
51f697c792
commit
2c8a5bdd25
113
index.html
113
index.html
|
@ -17,18 +17,6 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#seamap .seamap-marker .light {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: background ease-in-out 50ms;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
#seamap .seamap-marker .light.on {
|
|
||||||
background: yellow;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -61,13 +49,15 @@
|
||||||
SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
|
SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="https://unpkg.com/leaflet@1.2.0/dist/leaflet.js"
|
<script src="https://unpkg.com/leaflet@1.2.0/dist/leaflet-src.js"></script>
|
||||||
integrity="sha512-lInM/apFSqyy1o6s89K4iQUKg6ppXEgsVxT35HbzUupEVRh2Eu9Wdl4tHj7dZO0s1uvplcYGmt3498TtHq+log=="
|
|
||||||
crossorigin=""></script>
|
|
||||||
<script src="https://unpkg.com/osmtogeojson@3.0.0-beta.2/osmtogeojson.js"
|
<script src="https://unpkg.com/osmtogeojson@3.0.0-beta.2/osmtogeojson.js"
|
||||||
integrity="sha384-O1DMEF/gKYhLsICYtozkRWjEr9OfkZzVawUjyOPtevnKB2S1BegNJO0R251Pfuwz"
|
integrity="sha384-O1DMEF/gKYhLsICYtozkRWjEr9OfkZzVawUjyOPtevnKB2S1BegNJO0R251Pfuwz"
|
||||||
crossorigin=""></script>
|
crossorigin=""></script>
|
||||||
|
<script src="https://unpkg.com/rbush@2.0.1/rbush.js"></script>
|
||||||
<script src="https://unpkg.com/@turf/turf@3.5.2/turf.min.js"></script>
|
<script src="https://unpkg.com/@turf/turf@3.5.2/turf.min.js"></script>
|
||||||
|
<script src="leaflet.indexedfeaturelayer.js"></script>
|
||||||
|
<script src="leaflet.rangedmarker.js"></script>
|
||||||
|
<script src="leaflet.light.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let map = L.map('seamap').setView([54.2, 2.6], 6);
|
let map = L.map('seamap').setView([54.2, 2.6], 6);
|
||||||
|
|
||||||
|
@ -84,88 +74,12 @@
|
||||||
return [sw.lat, sw.lng, ne.lat, ne.lng]
|
return [sw.lat, sw.lng, ne.lat, ne.lng]
|
||||||
}
|
}
|
||||||
|
|
||||||
let Light = L.Icon.extend({
|
|
||||||
options: {
|
|
||||||
iconSize: [12, 12],
|
|
||||||
iconAnchor: [6, 6],
|
|
||||||
className: 'seamap-marker',
|
|
||||||
sequence: []
|
|
||||||
},
|
|
||||||
|
|
||||||
createIcon: function(icon) {
|
|
||||||
icon = document.createElement('div');
|
|
||||||
|
|
||||||
this._canvas = document.createElement('div');
|
|
||||||
this._canvas.className = 'light';
|
|
||||||
icon.appendChild(this._canvas);
|
|
||||||
|
|
||||||
this._setIconStyles(icon, 'icon');
|
|
||||||
|
|
||||||
return icon;
|
|
||||||
},
|
|
||||||
|
|
||||||
createShadow: function(icon) {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
_setIconStyles: function(icon, type) {
|
|
||||||
L.Icon.prototype._setIconStyles.apply(this, arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
setState: function(state) {
|
|
||||||
this._canvas.classList.toggle('on', !!state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
class Sequence {
|
|
||||||
constructor(seq) {
|
|
||||||
this.setSequence(seq);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSequence(seq) {
|
|
||||||
this.text = seq;
|
|
||||||
|
|
||||||
this.steps = seq.split('+').map(step => {
|
|
||||||
let state = true;
|
|
||||||
if (/^\(\d+(\.\d+)?\)$/.test(step)) {
|
|
||||||
state = false;
|
|
||||||
step = step.substring(1, step.length - 1);
|
|
||||||
}
|
|
||||||
return [state, parseFloat(step, 10)];
|
|
||||||
});
|
|
||||||
|
|
||||||
this.duration = this.steps.reduce((sum, step) => sum + step[1], 0);
|
|
||||||
|
|
||||||
this.offset = Math.random() * this.duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid() {
|
|
||||||
return this.steps.every(step => !isNaN(step[1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
state(time) {
|
|
||||||
if (isNaN(this.duration))
|
|
||||||
return undefined;
|
|
||||||
|
|
||||||
let dt = (this.offset + time) % this.duration;
|
|
||||||
|
|
||||||
for (let i = 0; i < this.steps.length; ++i) {
|
|
||||||
if (dt < this.steps[i][1])
|
|
||||||
return this.steps[i][0];
|
|
||||||
else
|
|
||||||
dt -= this.steps[i][1];
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Ran out of steps while still inside duration?');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let query = document.getElementById('seamap-query').textContent
|
let query = document.getElementById('seamap-query').textContent
|
||||||
.replace(/\{\{bbox\}\}/g, bbox(bounds).join(','));
|
.replace(/\{\{bbox\}\}/g, bbox(bounds).join(','));
|
||||||
|
|
||||||
let url = 'https://www.overpass-api.de/api/interpreter?data=' + encodeURIComponent(query);
|
let url = 'https://www.overpass-api.de/api/interpreter?data=' + encodeURIComponent(query);
|
||||||
|
|
||||||
url = 'data.json'; // For testing
|
url = 'data-full.json'; // For testing
|
||||||
|
|
||||||
let data = fetch(url)
|
let data = fetch(url)
|
||||||
.then(req => req.json())
|
.then(req => req.json())
|
||||||
|
@ -180,23 +94,26 @@
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let lights = data.then(geojson => {
|
let lights = data.then(geojson => {
|
||||||
return L.geoJSON(geojson, {
|
return L.indexedGeoJSON(null, {
|
||||||
pointToLayer: function(feat, latlng) {
|
pointToLayer: function(feat, latlng) {
|
||||||
return L.marker(latlng, {
|
return new L.Light(latlng, {
|
||||||
interactive: false,
|
interactive: false,
|
||||||
title: feat.properties.tags['name'],
|
title: feat.properties.tags['name'],
|
||||||
icon: new Light(),
|
radius: (parseFloat(feat.properties.tags['seamark:light:range'], 10) || 1) * 1000,
|
||||||
sequence: new Sequence(feat.properties.tags['seamark:light:sequence'])
|
sequence: new L.Light.Sequence(feat.properties.tags['seamark:light:sequence']),
|
||||||
|
stroke: false,
|
||||||
|
fillOpacity: 0.9,
|
||||||
|
fillColor: '#FF0'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).addTo(map);
|
}).addTo(map).addData(geojson);
|
||||||
});
|
});
|
||||||
|
|
||||||
lights.then(layer => {
|
lights.then(layer => {
|
||||||
let draw = function(t) {
|
let draw = function(t) {
|
||||||
layer.eachLayer(marker => {
|
layer.eachLayer(marker => {
|
||||||
try {
|
try {
|
||||||
marker.options.icon.setState(marker.options.sequence.state(t));
|
marker.setState(marker.options.sequence.state(t));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e, marker);
|
console.error(e, marker);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
function getBoundsWithPadding(map, padding) {
|
||||||
|
const bounds = map.getPixelBounds(),
|
||||||
|
sw = map.unproject(bounds.getBottomLeft().add([-padding, padding])),
|
||||||
|
ne = map.unproject(bounds.getTopRight().add([padding, -padding]));
|
||||||
|
|
||||||
|
return new L.LatLngBounds(sw, ne);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(L.LatLngBounds.prototype, {
|
||||||
|
toMinMax: function() {
|
||||||
|
return {
|
||||||
|
minX: this.getWest(),
|
||||||
|
minY: this.getSouth(),
|
||||||
|
maxX: this.getEast(),
|
||||||
|
maxY: this.getNorth()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.LayerGroup.include({
|
||||||
|
updateLayers: function(layers) {
|
||||||
|
var _layers = {};
|
||||||
|
|
||||||
|
for (var i = 0; i < layers.length; ++i)
|
||||||
|
_layers[this.getLayerId(layers[i])] = layers[i];
|
||||||
|
|
||||||
|
var toRemove = [];
|
||||||
|
|
||||||
|
for (var id in this._layers) {
|
||||||
|
if (!(id in _layers))
|
||||||
|
toRemove.push(this._layers[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
toRemove.forEach(this.removeLayer, this);
|
||||||
|
|
||||||
|
for (var id in _layers) {
|
||||||
|
if (!(id in this._layers))
|
||||||
|
this.addLayer(_layers[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.IndexedFeatureLayer = L.GeoJSON.extend({
|
||||||
|
options: {
|
||||||
|
padding: 30 // in pixels
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function (geojson, options) {
|
||||||
|
L.Util.setOptions(this, options);
|
||||||
|
|
||||||
|
this._layers = {};
|
||||||
|
|
||||||
|
this._visible = L.layerGroup([]);
|
||||||
|
|
||||||
|
this._rbush = rbush(9);
|
||||||
|
|
||||||
|
if (geojson) {
|
||||||
|
this.addData(geojson);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
search: function(bounds) {
|
||||||
|
return this._rbush.search(bounds.toMinMax()).map(result => result.layer);
|
||||||
|
},
|
||||||
|
|
||||||
|
getLayerId: function(layer) {
|
||||||
|
return layer.feature.id;
|
||||||
|
},
|
||||||
|
|
||||||
|
addLayer: function (layer) {
|
||||||
|
const id = this.getLayerId(layer);
|
||||||
|
|
||||||
|
if (id in this._layers)
|
||||||
|
return this;
|
||||||
|
|
||||||
|
this._layers[id] = layer;
|
||||||
|
|
||||||
|
// Necessary for circle markers I use here
|
||||||
|
layer._map = this._map;
|
||||||
|
layer._project();
|
||||||
|
|
||||||
|
const xy = layer.getBounds().toMinMax();
|
||||||
|
this._rbush.insert(Object.assign({layer: layer}, xy));
|
||||||
|
|
||||||
|
if (this._map
|
||||||
|
&& !this._visible.hasLayer(layer)
|
||||||
|
&& this._layerInView(layer)) {
|
||||||
|
layer._map = null;
|
||||||
|
this._visible.addLayer(layer);
|
||||||
|
} else {
|
||||||
|
layer._map = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
this._visible.addTo(map);
|
||||||
|
map.on('moveend', this._redraw, this);
|
||||||
|
this._redraw();
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function(map) {
|
||||||
|
this._visible.removeFrom(map);
|
||||||
|
},
|
||||||
|
|
||||||
|
_getBounds: function() {
|
||||||
|
return getBoundsWithPadding(this._map, this.options.padding);
|
||||||
|
},
|
||||||
|
|
||||||
|
_redraw: function() {
|
||||||
|
const layers = this.search(this._getBounds());
|
||||||
|
console.log(layers.length, 'layers');
|
||||||
|
this._visible.updateLayers(layers);
|
||||||
|
},
|
||||||
|
|
||||||
|
_layerInView: function(layer) {
|
||||||
|
return layer.getBounds().intersects(this._getBounds());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.indexedGeoJSON = function(geojson, options) {
|
||||||
|
return new L.IndexedFeatureLayer(geojson, options);
|
||||||
|
};
|
|
@ -0,0 +1,51 @@
|
||||||
|
L.Light = L.Circle.extend({
|
||||||
|
setState: function(state) {
|
||||||
|
if (this._state !== state) {
|
||||||
|
L.Path.prototype.setStyle.call(this, {fill: !!state});
|
||||||
|
this._state = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.Light.Sequence = class {
|
||||||
|
constructor(seq) {
|
||||||
|
this.setSequence(seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSequence(seq) {
|
||||||
|
this.text = seq;
|
||||||
|
|
||||||
|
this.steps = seq.split('+').map(step => {
|
||||||
|
let state = true;
|
||||||
|
if (/^\(\d+(\.\d+)?\)$/.test(step)) {
|
||||||
|
state = false;
|
||||||
|
step = step.substring(1, step.length - 1);
|
||||||
|
}
|
||||||
|
return [state, parseFloat(step, 10)];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.duration = this.steps.reduce((sum, step) => sum + step[1], 0);
|
||||||
|
|
||||||
|
this.offset = Math.random() * this.duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
return this.steps.every(step => !isNaN(step[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
state(time) {
|
||||||
|
if (isNaN(this.duration))
|
||||||
|
return undefined;
|
||||||
|
|
||||||
|
let dt = (this.offset + time) % this.duration;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.steps.length; ++i) {
|
||||||
|
if (dt < this.steps[i][1])
|
||||||
|
return this.steps[i][0];
|
||||||
|
else
|
||||||
|
dt -= this.steps[i][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Ran out of steps while still inside duration?');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
L.RangedMarker = L.Marker.extend({
|
||||||
|
options: {
|
||||||
|
range: 1
|
||||||
|
},
|
||||||
|
initialize: function(latlng, options) {
|
||||||
|
L.Marker.prototype.initialize.call(this, latlng, options);
|
||||||
|
|
||||||
|
const updateIconSize = this.updateIconSize.bind(this);
|
||||||
|
this.updateCallback = function() {
|
||||||
|
updateIconSize(this);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onAdd: function(map) {
|
||||||
|
L.Marker.prototype.onAdd.call(this, map);
|
||||||
|
map.on('zoomend', this.updateCallback);
|
||||||
|
this.updateIconSize(map);
|
||||||
|
},
|
||||||
|
onRemove: function(map) {
|
||||||
|
map.off('zoomend', this.updateCallback);
|
||||||
|
L.Marker.prototype.onRemove.call(this, map);
|
||||||
|
},
|
||||||
|
updateIconSize: function(map) {
|
||||||
|
let size = this._getSizeOnMap(map);
|
||||||
|
this._icon.style.width = size + 'px';
|
||||||
|
this._icon.style.height = size + 'px';
|
||||||
|
},
|
||||||
|
_getSizeOnMap: function (map) {
|
||||||
|
return this.options.range / this._getMetersPerPixel(map);
|
||||||
|
},
|
||||||
|
_getMetersPerPixel: function(map) {
|
||||||
|
var centerLatLng = map.getCenter(); // get map center
|
||||||
|
var pointC = map.latLngToContainerPoint(centerLatLng); // convert to containerpoint (pixels)
|
||||||
|
var pointX = L.point(pointC.x + 10, pointC.y); // add 10 pixels to x
|
||||||
|
|
||||||
|
// convert containerpoints to latlng's
|
||||||
|
var latLngX = map.containerPointToLatLng(pointX);
|
||||||
|
return centerLatLng.distanceTo(latLngX) / 10; // calculate distance between c and x (latitude)
|
||||||
|
}
|
||||||
|
});
|
Ładowanie…
Reference in New Issue