From 98d44fd1337e1f2e408b7de11038d153a59ca32c Mon Sep 17 00:00:00 2001 From: Jelmer van der Linde Date: Sat, 16 Sep 2017 09:56:02 +0200 Subject: [PATCH] More precise parsing of seamark:light tags --- index.html | 26 +++++--- leaflet.light.js | 169 ++++++++++++++++++++++++++++++++++++++++++----- test.json | 164 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 333 insertions(+), 26 deletions(-) create mode 100644 test.json diff --git a/index.html b/index.html index 33c8069..51eaf59 100644 --- a/index.html +++ b/index.html @@ -96,14 +96,28 @@ let lights = data.then(geojson => { return L.indexedGeoJSON(null, { pointToLayer: function(feat, latlng) { + let sequence; + + try { + sequence = L.Light.sequence(feat.properties.tags, '#FF0'); + } catch (e) { + console.error('Error parsing sequence: %s', e, feat.properties.tags); + + // Fallback sequence + sequence = L.Light.sequence({ + 'seamark:light:sequence': '1+(1)' + }); + } + return new L.Light(latlng, { interactive: false, title: feat.properties.tags['name'], radius: (parseFloat(feat.properties.tags['seamark:light:range'], 10) || 1) * 1000, - sequence: new L.Light.Sequence(feat.properties.tags['seamark:light:sequence']), + sequence: sequence, stroke: false, fillOpacity: 0.9, - fillColor: '#FF0' + fill: !!sequence.state(0), + fillColor: sequence.state(0) }); } }).addTo(map).addData(geojson); @@ -112,11 +126,7 @@ lights.then(layer => { let draw = function(t) { layer.eachVisibleLayer(marker => { - try { - marker.setState(marker.options.sequence.state(t)); - } catch (e) { - console.error(e, marker); - } + marker.setColor(marker.options.sequence.state(t)); }); }; @@ -126,7 +136,7 @@ }; update(0); - }) + }).catch(e => console.error(e)); lights.catch(e => console.error(e)); diff --git a/leaflet.light.js b/leaflet.light.js index 0146546..8cbfc63 100644 --- a/leaflet.light.js +++ b/leaflet.light.js @@ -1,22 +1,159 @@ L.Light = L.Circle.extend({ - setState: function(state) { - if (this._state !== state) { - L.Path.prototype.setStyle.call(this, {fill: !!state}); - this._state = state; + setColor(color) { + if (this._color !== color) { + this._color = color; + L.Path.prototype.setStyle.call(this, {fill: !!color, fillColor: color}); } } }); -L.Light.Sequence = class { - constructor(seq) { - this.setSequence(seq); +L.Light.sequence = function(tags, fallbackColor = '#FF0') { + let character = tags['seamark:light:character'] || 'Fl'; + + let colors = (tags['seamark:light:colour'] || fallbackColor).split(';'); + + let sequence = tags['seamark:light:sequence']; + + if (character.match(/^Al\./)) {// Alternating color! + character = tags['seamark:light:character'].substring(3); + + if (character == 'Iso' && sequence && sequence.match(/^\d+$/)) + sequence = sequence + '+(' + sequence + ')'; } - setSequence(seq) { - this.text = seq; + if (character == 'Iso' && !sequence && 'seamark:light:period' in tags) { + const period = parseFloat(tags['seamark:light:period'], 10); + sequence = (period / 2) + '+(' + (period / 2) + ')'; + } + + // For those Flashing lights that have a single number sequence + if (character.match(/^Fl|LFl|IQ$/) && sequence.match(/^\d+$/)) { + const flash = parseFloat(sequence) + const remainder = 'seamark:light:period' in tags ? (parseFloat(tags['seamark:light:period']) - flash) : flash; + character = 'Fl'; + sequence = flash + '+(' + remainder + ')'; + } + + // Convert FFl to Fl + if (character == 'FFl' && sequence.match(/^\d+$/) && tags['seamark:light:period'].match(/^\d+$/)) { + character = 'Fl'; + sequence = parseFloat(sequence, 10) + '+(' + (parseFloat(tags['seamark:light:period'], 10) - parseFloat(sequence, 10)) + ')'; + } + + // Convert Q with Q+LFL sequence to Fl + if (character == 'Q' && 'seamark:light:period' in tags && sequence.match(/^Q(\(\d+\))?\s*\+\s*LFL/)) { + let qlfl = sequence.match(/^Q(\((\d+)\))?\s*\+\s*LFL/); + const period = parseFloat(tags['seamark:light:period']); + const short = parseFloat(qlfl[2] || tags['seamark:light:group'] || 1); + const long = 1; + const flash = 0.2; + const longflash = 1.0; + const remainder = period - (short * 2 * flash + longflash) - this.steps = seq.split('+').map(step => { - let state = true; + if (remainder < 0) + throw 'Could not convert Q+LFL to Fl: negative remainder'; + + character = 'Fl'; + sequence = Array(short).fill(flash + '+(' + flash + ')').join('+') + '+' + longflash + '+(' + remainder + ')'; + } + + // Convert simple quick flashes which indicates how many with group and the total duration of that group with sequence into Fl. + if (character == 'Q' && sequence.match(/^\d$/) && 'seamark:light:group' in tags) { + const short = parseFloat(tags['seamark:light:group']); + const flash = parseFloat(sequence) / short / 2; + character = 'Fl'; + sequence = Array(short).fill(flash + '+(' + flash + ')').join('+'); + } + + // Remove the 'second' suffix + sequence = sequence.replace(/s$/, ''); + + switch (character) { + case 'F': // Fixed Light + return new L.Light.Fixed(colors[0]); + + case 'Iso': + return new L.Light.CombinedSequence(colors.map(color => { + return new L.Light.Sequence(sequence, color); + })); + + case 'Oc': // Occulting Light + case 'Fl': // Flashing Light + case 'LFl': // Long Flash Light + case 'Q': // Quick Flashing Light + case 'Mo': + if (!sequence || sequence.match(/^\d+$/)) + throw 'Unexpected sequence: ' + sequence; + + const sequences = sequence.split(',').map((sequence, i) => { + let color = colors[i % colors.length]; + + // Does the sequence start with the color? + let letter = sequence.match(/^\[([A-Z]+)\.\](.+)$/); + if (letter) { + const expr = new RegExp('^' + letter[1], 'i'); + color = colors.find(color => color.match(expr)); + sequence = letter[2]; + } + + return new L.Light.Sequence(sequence, color); + }); + + if (sequences.length < colors.length) + console.warn('There are fewer sequences than colors', {character, sequence, colors}, tags); + + return new L.Light.CombinedSequence(sequences); + + default: + throw 'Unknown character: ' + character + } +} + +L.Light.Fixed = class { + constructor(color) { + this.color = color; + } + + state(time) { + return this.color; + } +} + +L.Light.CombinedSequence = class { + constructor(sequences) { + this.sequences = sequences; + + this.sequences.forEach(sequence => { + sequence.offset = 0; + }); + + this.duration = this.sequences.reduce((sum, seq) => sum + seq.duration, 0); + + this.offset = Math.random() * this.duration; + } + + state(time) { + let dt = (this.offset + time) % this.duration; + let i = 0; + + while (dt > this.sequences[i].duration) { + dt -= this.sequences[i++].duration; + } + + return this.sequences[i].state(dt); + } +} + +L.Light.Sequence = class { + constructor(seq, color=true) { + this.setSequence(seq, color); + } + + setSequence(seq, color) { + this.text = seq; + + this.steps = seq.replace(/\s/g, '').split('+').map(step => { + let state = color; if (/^\(\d+(\.\d+)?\)$/.test(step)) { state = false; step = step.substring(1, step.length - 1); @@ -26,17 +163,13 @@ L.Light.Sequence = class { this.duration = this.steps.reduce((sum, step) => sum + step[1], 0); + if (isNaN(this.duration)) + throw 'Cannot parse sequence "' + this.text + '"'; + 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) { diff --git a/test.json b/test.json new file mode 100644 index 0000000..8a72685 --- /dev/null +++ b/test.json @@ -0,0 +1,164 @@ +{ + "version": 0.6, + "generator": "Overpass API", + "osm3s": { + "timestamp_osm_base": "2017-08-31T18:02:02Z", + "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL." + }, + "elements": [ +{ + "type": "node", + "id": 1191081664, + "lat": -41.1750000, + "lon": 146.4270000, + "tags": { + "seamark:information": "Occasional.", + "seamark:light:category": "aero", + "seamark:light:character": "Al.Fl", + "seamark:light:colour": "green;white", + "seamark:light:height": "23", + "seamark:light:period": "7.4", + "seamark:light:reference": "K 3563", + "seamark:light:sequence": "[W.]0.2+(3.5),[G.]0.2+(3.5)", + "seamark:type": "light_minor", + "source": "US NGA Pub. 111. 2010-09-02." + } +}, +{ + "type": "node", + "id": 1347582795, + "lat": 40.6590033, + "lon": 17.9417167, + "tags": { + "seamark:information": "Private light.", + "seamark:light:character": "Al.Fl", + "seamark:light:colour": "white;green", + "seamark:light:height": "18", + "seamark:light:period": "17", + "seamark:light:reference": "E 2208", + "seamark:light:sequence": "[W.]1+(3),[G.]1+(3),[W.]1+(8)", + "seamark:name": "Brindisi-Casale", + "seamark:type": "light_major", + "source": "US NGA Pub. 113. 2010-10-22." + } +}, +{ + "type": "node", + "id": 1581589349, + "lat": 32.4032167, + "lon": 34.8670833, + "tags": { + "seamark:light:character": "Al.Fl", + "seamark:light:colour": "white;red", + "seamark:light:height": "14", + "seamark:light:period": "15", + "seamark:light:range": "10", + "seamark:light:reference": "E 5956", + "seamark:light:sequence": "0.5+(4.5),0.5+(9.5)", + "seamark:name": "Mikhmoret", + "seamark:type": "light_minor", + "source": "US NGA Pub. 113. 2011-10-20." + } +}, +{ + "type": "node", + "id": 1581742946, + "lat": 34.2909833, + "lon": -6.6007500, + "tags": { + "seamark:information": "A F.R. light is shown from a radio tower, about 15.6 km. 194° .Occasional.", + "seamark:light:category": "aero", + "seamark:light:character": "Al.Fl", + "seamark:light:colour": "white;green", + "seamark:light:group": "2+1", + "seamark:light:height": "55", + "seamark:light:period": "10", + "seamark:light:reference": "D 2541", + "seamark:light:sequence": "1+(3.5)", + "seamark:name": "Kenitra", + "seamark:type": "light_major", + "source": "US NGA Pub. 113. 2011-10-20." + } +}, +{ + "type": "node", + "id": 2153785316, + "lat": 58.7372741, + "lon": 17.1029072, + "tags": { + "seamark:light:character": "Al.Iso", + "seamark:light:colour": "white;red", + "seamark:light:sequence": "3", + "seamark:type": "light_major" + } +}, +{ + "type": "node", + "id": 2153785362, + "lat": 58.7405845, + "lon": 17.0841161, + "tags": { + "seamark:light:character": "Iso", + "seamark:light:colour": "red", + "seamark:light:sequence": "3", + "seamark:name": "Linudden nedre", + "seamark:type": "light_major" + } +}, +{ + "type": "node", + "id": 2300566678, + "lat": 50.9707556, + "lon": 1.8400850, + "tags": { + "seamark:fog_signal:category": "bell", + "seamark:fog_signal:period": "5", + "seamark:light:character": "Iso", + "seamark:light:colour": "green", + "seamark:light:group": "2", + "seamark:light:height": "11.90", + "seamark:light:period": "3", + "seamark:light:range": "9", + "seamark:light:reference": "A 1148", + "seamark:light:sequence": "1.5+(1.5)", + "seamark:name": "Jetée Ouest", + "seamark:type": "light_minor", + "source": "US NGA Pub. 114. 2011-04-02", + "source:website": "http://www.pharesdumonde.fr/details.php?l=FRA&n=654&z=1&li=2" + } +}, +{ + "type": "node", + "id": 3028106017, + "lat": 48.2790445, + "lon": -4.5885642, + "tags": { + "seamark:light:character": "Fl", + "seamark:light:colour": "green", + "seamark:light:group": "2", + "seamark:light:height": "9", + "seamark:light:period": "6", + "seamark:light:range": "5", + "seamark:light:sequence": "1+(1),1+(3)", + "seamark:type": "light_minor" + } +}, +{ + "type": "node", + "id": 3041544410, + "lat": 46.1482397, + "lon": -1.1697761, + "tags": { + "seamark:light:character": "Fl", + "seamark:light:colour": "green", + "seamark:light:group": "2", + "seamark:light:height": "9", + "seamark:light:period": "6", + "seamark:light:range": "5", + "seamark:light:reference": "D 1254", + "seamark:light:sequence": "1+(1),1+(3)", + "seamark:type": "light_minor" + } +} +] +} \ No newline at end of file