Browse Source

Render ALL the lighthouses!

pull/3/head
Jelmer van der Linde 5 years ago
parent
commit
2c8a5bdd25
  1. 119
      index.html
  2. 126
      leaflet.indexedfeaturelayer.js
  3. 51
      leaflet.light.js
  4. 39
      leaflet.rangedmarker.js

119
index.html

@ -17,18 +17,6 @@
width: 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>
</head>
<body>
@ -54,20 +42,22 @@
?item wdt:P31 wd:Q39715.
?item wdt:P625 ?location.
OPTIONAL {
?item wdt:P2048 ?height.
?item wdt:P2923 ?focalHeight.
?item wdt:P1030 ?sequence.
?item wdt:P2048 ?height.
?item wdt:P2923 ?focalHeight.
?item wdt:P1030 ?sequence.
}
SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". }
}
</script>
<script src="https://unpkg.com/leaflet@1.2.0/dist/leaflet.js"
integrity="sha512-lInM/apFSqyy1o6s89K4iQUKg6ppXEgsVxT35HbzUupEVRh2Eu9Wdl4tHj7dZO0s1uvplcYGmt3498TtHq+log=="
crossorigin=""></script>
<script src="https://unpkg.com/leaflet@1.2.0/dist/leaflet-src.js"></script>
<script src="https://unpkg.com/osmtogeojson@3.0.0-beta.2/osmtogeojson.js"
integrity="sha384-O1DMEF/gKYhLsICYtozkRWjEr9OfkZzVawUjyOPtevnKB2S1BegNJO0R251Pfuwz"
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="leaflet.indexedfeaturelayer.js"></script>
<script src="leaflet.rangedmarker.js"></script>
<script src="leaflet.light.js"></script>
<script>
let map = L.map('seamap').setView([54.2, 2.6], 6);
@ -84,88 +74,12 @@
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
.replace(/\{\{bbox\}\}/g, bbox(bounds).join(','));
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)
.then(req => req.json())
@ -180,23 +94,26 @@
}));
let lights = data.then(geojson => {
return L.geoJSON(geojson, {
return L.indexedGeoJSON(null, {
pointToLayer: function(feat, latlng) {
return L.marker(latlng, {
return new L.Light(latlng, {
interactive: false,
title: feat.properties.tags['name'],
icon: new Light(),
sequence: new Sequence(feat.properties.tags['seamark:light:sequence'])
radius: (parseFloat(feat.properties.tags['seamark:light:range'], 10) || 1) * 1000,
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 => {
let draw = function(t) {
layer.eachLayer(marker => {
try {
marker.options.icon.setState(marker.options.sequence.state(t));
marker.setState(marker.options.sequence.state(t));
} catch (e) {
console.error(e, marker);
}

126
leaflet.indexedfeaturelayer.js

@ -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);
};

51
leaflet.light.js

@ -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?');
}
}

39
leaflet.rangedmarker.js

@ -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)
}
});
Loading…
Cancel
Save