├── .eslintrc ├── LICENSE ├── README.md ├── aed.html ├── ivb.html ├── map.ivb.js ├── map.js ├── prettier.config.js ├── railway.html ├── restaurant.html ├── semicircle.js └── wiwosm.html /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "prefer-arrow-callback": "warn", 4 | "prefer-const": "warn", 5 | "prefer-destructuring": "warn", 6 | "prefer-template": "warn" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2012-2016, Simon Legner 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # POImap 2 | 3 | Lightweight POI (point of interest) maps based on [OpenStreetMap](http://www.openstreetmap.org/), [Leaflet](http://leaflet.cloudmade.com/) and [Overpass API](http://www.overpass-api.de/). 4 | 5 | - [Automated external defibrillator map](http://simon04.github.io/POImap/aed.html) (loading all data at once) 6 | - [Restaurant map](http://simon04.github.io/POImap/restaurant.html) (loading data from current bbox) 7 | - [Network of routes of Innsbrucker Verkehrsbetriebe](http://simon04.github.io/POImap/ivb.html) (generating GeoJSON out of transport relations) 8 | - [Railway map](http://simon04.github.io/POImap/railway.html) (loading data from current bbox, generating GeoJSON out of ways) 9 | -------------------------------------------------------------------------------- /aed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AED Map 5 | 6 | 7 | 8 | 9 | 10 | 50 | 51 | 62 | 63 | 64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /ivb.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | IVB Map 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 134 | 135 | 136 |
137 | 138 | 139 | -------------------------------------------------------------------------------- /map.ivb.js: -------------------------------------------------------------------------------- 1 | var IVB = {}; 2 | IVB.layers = {}; 3 | 4 | IVB.init = () => { 5 | // init map 6 | IVB.map = POImap.init(); 7 | IVB.map.setZoom(15); 8 | // init stop labels layer 9 | IVB.layers.stopLabels = L.layerGroup().addTo(IVB.map); 10 | IVB.map.getControl().addOverlay(IVB.layers.stopLabels, 'Haltestellen'); 11 | IVB.layers.stopCatchment = L.layerGroup().addTo(IVB.map); 12 | IVB.map.getControl().addOverlay(IVB.layers.stopCatchment, 'Einzugsgebiet'); 13 | // init proposed line extensions 14 | IVB.layers.proposed = L.layerGroup(); 15 | IVB.map.getControl().addOverlay(IVB.layers.proposed, 'Erweiterung'); 16 | IVB.layers.frequency = L.layerGroup(); 17 | IVB.map.getControl().addOverlay(IVB.layers.frequency, 'Frequenz'); 18 | L.control.scale({imperial: false}).addTo(IVB.map); 19 | IVB.frequencyBySegment = L.layerGroup(); 20 | var extensionUrl = 21 | 'https://www.overpass-api.de/api/interpreter?data=[out:json];(way(47.2,11.3,47.3,11.6)[railway~"construction|proposed"][construction!=rail];node(w););out body;'; 22 | POImap.loadAndParseOverpassJSON( 23 | extensionUrl, 24 | null, 25 | IVB.displayExtension(IVB.layers.proposed), 26 | null 27 | ); 28 | // load route relations 29 | var linesUrl = 30 | 'https://www.overpass-api.de/api/interpreter?data=[out:json];' + 31 | '(relation["operator:short"="IVB"][type=route]["public_transport:version"=2];node(r)->.x;way(r);node(w););out body;'; 32 | POImap.loadAndParseOverpassJSON( 33 | linesUrl, 34 | null, 35 | null, 36 | IVB.handleRelation, 37 | IVB.addSegmentFrequencies 38 | ); 39 | }; 40 | 41 | IVB.displayExtension = (layer) => (w) => { 42 | L.geoJson( 43 | { 44 | type: 'Feature', 45 | geometry: w.geometry, 46 | }, 47 | { 48 | style: (e) => ({ 49 | opacity: 1, 50 | color: '#999', 51 | //svg: {'stroke-dasharray': '6,8'}, 52 | weight: 3, 53 | }), 54 | } 55 | ).addTo(layer); 56 | }; 57 | 58 | IVB.handleRelation = (p) => { 59 | var stopAngles = IVB.getStopAngles(p); 60 | p.members 61 | .filter((mem) => mem.obj && mem.role != 'platform') 62 | .map((mem) => { 63 | IVB.addLine(p.tags.ref, { 64 | type: 'Feature', 65 | geometry: mem.obj.geometry, 66 | id: mem.obj.id, 67 | tags: mem.obj.tags, 68 | reltags: p.tags, 69 | angle: stopAngles[mem.ref], 70 | }); 71 | if (p.tags.interval && mem.role === '' && !/N[0-9]/.test(p.tags.ref)) { 72 | if (!IVB.frequencyBySegment[mem.obj.id]) { 73 | IVB.frequencyBySegment[mem.obj.id] = { 74 | type: 'Feature', 75 | geometry: mem.obj.geometry, 76 | id: mem.obj.id, 77 | routes: {}, 78 | frequency: 0, 79 | }; 80 | } 81 | var frequency = 60 / p.tags.interval; 82 | IVB.frequencyBySegment[mem.obj.id].frequency += frequency; 83 | IVB.frequencyBySegment[mem.obj.id].routes[p.tags.ref] = 1; 84 | } 85 | }); 86 | }; 87 | 88 | IVB.getStopAngles = (relation) => { 89 | var stopAngle = {}; 90 | var getAngleForStop = (stop) => { 91 | relation.members.map((mem) => { 92 | if (mem.type != 'way') return; 93 | // index of node in way 94 | var idx = mem.obj.nodes.indexOf(stop); 95 | if (idx < 0) return; 96 | // take two adjacent ways 97 | // ignore projection and use lat/lon directly (shouldn't make a big difference as 2 nodes are rather close) 98 | var lonlat1 = mem.obj.coordinates[idx === 0 ? 0 : idx - 1]; 99 | var lonlat2 = mem.obj.coordinates[idx === 0 ? 1 : idx]; 100 | // compute direction, i.e., angle (with mathematical meaning: 0=horizontal, anti-clockwise) 101 | var angle = (Math.atan2(lonlat2[1] - lonlat1[1], lonlat2[0] - lonlat1[0]) * 180) / Math.PI; 102 | // determine in which order those two nodes are used 103 | var wayMembers = relation.members.filter((r) => { 104 | return r.obj.type == 'way' && r.role != 'platform'; 105 | }); 106 | var idxWay = wayMembers.indexOf(mem); 107 | if (idxWay < 0) { 108 | // ignore 109 | } else { 110 | // assumes ways in a block, i.e., w/o stops in between 111 | // if first node linked to previous way then +1 else -1 112 | var way1 = idxWay === 0 ? wayMembers[0] : wayMembers[idxWay - 1]; 113 | var way2 = idxWay === 0 ? wayMembers[1] : wayMembers[idxWay]; 114 | if (!way1.obj.nodes || !way2.obj.nodes) return; 115 | var way1Before2 = way2.obj.nodes.indexOf(way1.obj.nodes[0]) === -1; 116 | angle += way1Before2 ? 0 : 180; 117 | } 118 | //console.log(relation.tags.name, stop, mem.obj, idx, idx == 0 ? 0 : idx - 1, idx == 0 ? 1 : idx, lonlat1, lonlat2, idxWay, polarity, angle); 119 | stopAngle[stop] = Math.round(angle); 120 | }); 121 | }; 122 | 123 | var stops = relation.members 124 | .filter( 125 | (mem) => 126 | mem.type == 'node' && 127 | mem.role.match(/stop/) /* && mem.obj.tags.name && mem.obj.tags.name.match(/Klinik/);*/ 128 | ) 129 | .map((mem) => mem.ref) 130 | .map(getAngleForStop); 131 | return stopAngle; 132 | }; 133 | 134 | IVB.halts = {}; 135 | IVB.addLine = (lineref, geojson) => { 136 | var layer = L.geoJson(geojson, { 137 | style: (p) => { 138 | // Adapt style/color 139 | var color = p.reltags.colour || p.reltags.color || '000000'; 140 | return {opacity: 1, color: color.charAt(0) == '#' ? color : `#${color}`}; 141 | }, 142 | pointToLayer: IVB.addStop, 143 | onEachFeature: IVB.bindPopup, 144 | }).addTo(IVB.layers[lineref] || IVB.map); 145 | if (!IVB.layers[lineref]) { 146 | IVB.layers[lineref] = layer; 147 | IVB.map.getControl().addOverlay(layer, `Linie ${lineref}`); 148 | } 149 | }; 150 | 151 | // Displaying of points, e.g., halts 152 | IVB.addStop = (data, latlng) => { 153 | // Adapt handling of duplicate stations of same line 154 | if (data.tags && data.tags.name && data.id != 287054151) { 155 | data.tags.name = data.tags.name.replace(/^Innsbruck /, ''); 156 | var id = [data.reltags.ref, data.tags.name].join('/'); 157 | if (!IVB.halts[id]) { 158 | // Adapt css classes of halt name 159 | var className = [ 160 | 'leaflet-div-icon', 161 | `L${data.reltags.ref}`, 162 | data.tags && data.tags.name 163 | ? data.tags.name.replace(/\/| /g, '-').replace(/\(|\)/g, '') 164 | : '', 165 | ].join(' '); 166 | // Add halt name as DivIcon 167 | L.marker(latlng, { 168 | icon: L.divIcon({className: className, html: data.tags.name || ''}), 169 | }).addTo(IVB.layers.stopLabels); 170 | IVB.halts[id] = true; 171 | } 172 | if (!IVB.halts[data.tags.name]) { 173 | L.circle(latlng, 300, {color: '#666', weight: 2}).addTo(IVB.layers.stopCatchment); 174 | IVB.halts[data.tags.name] = true; 175 | } 176 | } 177 | // Add/return halt as CircleMarker 178 | return typeof data.angle === 'undefined' 179 | ? new L.CircleMarker(latlng, {fillOpacity: 1, weight: 0}).setRadius(4) 180 | : new L.SemicircleMarker(latlng, {fillOpacity: 1, weight: 0}) 181 | .setRadius(8) 182 | .setAngle(-1 * data.angle); 183 | }; 184 | 185 | IVB.bindPopup = (p, l) => { 186 | // Adapt popup 187 | l.bindPopup(`${p.reltags.ref}${p.tags && p.tags.name ? ' ' + p.tags.name : ''}`); 188 | }; 189 | 190 | IVB.addSegmentFrequencies = () => { 191 | Object.keys(IVB.frequencyBySegment).map((id) => { 192 | var segment = IVB.frequencyBySegment[id]; 193 | if (segment.type !== 'Feature') return; 194 | var routes = Object.keys(segment.routes).sort().join(', '); 195 | L.geoJson(segment, { 196 | style: () => ({ 197 | opacity: 1, 198 | color: '#000000', 199 | weight: segment.frequency || 1, 200 | }), 201 | }) 202 | .bindTooltip(`${segment.frequency} Fahrten pro Stunde
Linien: ${routes}`) 203 | .addTo(IVB.layers.frequency); 204 | }); 205 | }; 206 | -------------------------------------------------------------------------------- /map.js: -------------------------------------------------------------------------------- 1 | var POImap = {}; 2 | 3 | POImap.init = () => { 4 | var attr_osm = 'Map data © OpenStreetMap contributors', 5 | attr_overpass = 'POI via Overpass API'; 6 | 7 | var osm = new L.TileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 8 | attribution: [attr_osm, attr_overpass].join(', '), 9 | }), 10 | transport = new L.TileLayer('https://{s}.tile2.opencyclemap.org/transport/{z}/{x}/{y}.png', { 11 | opacity: 0.5, 12 | attribution: [ 13 | 'Gravitystorm Transport Map', 14 | attr_osm, 15 | attr_overpass, 16 | ].join(', '), 17 | }), 18 | osm_bw = new L.TileLayer('https://{s}.www.toolserver.org/tiles/bw-mapnik/{z}/{x}/{y}.png', { 19 | opacity: 0.5, 20 | attribution: [attr_osm, attr_overpass].join(', '), 21 | }), 22 | osm_no = new L.TileLayer('https://{s}.www.toolserver.org/tiles/osm-no-labels/{z}/{x}/{y}.png', { 23 | attribution: [attr_osm, attr_overpass].join(', '), 24 | }); 25 | 26 | map = new L.Map('map', { 27 | center: new L.LatLng(47.2632776, 11.4010086), 28 | zoom: 13, 29 | layers: osm, 30 | }); 31 | 32 | map.getControl = (() => { 33 | var ctrl = new L.Control.Layers({ 34 | OpenStreetMap: osm, 35 | 'OpenStreetMap (no labels)': osm_no, 36 | 'OpenStreetMap (black/white)': osm_bw, 37 | 'Transport Map': transport, 38 | }); 39 | return () => ctrl; 40 | })(); 41 | map.addControl(map.getControl()); 42 | 43 | L.LatLngBounds.prototype.toOverpassBBoxString = function () { 44 | var a = this._southWest, 45 | b = this._northEast; 46 | return [a.lat, a.lng, b.lat, b.lng].join(','); 47 | }; 48 | 49 | var path_style = L.Path.prototype._updateStyle; 50 | L.Path.prototype._updateStyle = function () { 51 | path_style.apply(this); 52 | for (var k in this.options.svg) { 53 | this._path.setAttribute(k, this.options.svg[k]); 54 | } 55 | }; 56 | 57 | if (navigator.geolocation && !/ivb.html/.test(location.href)) { 58 | navigator.geolocation.getCurrentPosition((position) => { 59 | var center = new L.LatLng(position.coords.latitude, position.coords.longitude); 60 | map.setView(center, 13); 61 | }); 62 | } 63 | 64 | POImap.map = map; 65 | return map; 66 | }; 67 | 68 | POImap.loadAndParseOverpassJSON = ( 69 | overpassQueryUrl, 70 | callbackNode, 71 | callbackWay, 72 | callbackRelation, 73 | callbackDone 74 | ) => { 75 | var url = overpassQueryUrl.replace(/(BBOX)/g, map.getBounds().toOverpassBBoxString()); 76 | $.getJSON(url, (json) => { 77 | POImap.parseOverpassJSON(json, callbackNode, callbackWay, callbackRelation, callbackDone); 78 | }); 79 | }; 80 | 81 | POImap.parseOverpassJSON = ( 82 | overpassJSON, 83 | callbackNode, 84 | callbackWay, 85 | callbackRelation, 86 | callbackDone 87 | ) => { 88 | var nodes = {}, 89 | ways = {}; 90 | for (var i = 0; i < overpassJSON.elements.length; i++) { 91 | var p = overpassJSON.elements[i]; 92 | switch (p.type) { 93 | case 'node': 94 | p.coordinates = [p.lon, p.lat]; 95 | p.geometry = {type: 'Point', coordinates: p.coordinates}; 96 | nodes[p.id] = p; 97 | // p has type=node, id, lat, lon, tags={k:v}, coordinates=[lon,lat], geometry 98 | if (typeof callbackNode === 'function') callbackNode(p); 99 | break; 100 | case 'way': 101 | p.coordinates = p.nodes.map((id) => { 102 | return nodes[id].coordinates; 103 | }); 104 | p.geometry = {type: 'LineString', coordinates: p.coordinates}; 105 | ways[p.id] = p; 106 | // p has type=way, id, tags={k:v}, nodes=[id], coordinates=[[lon,lat]], geometry 107 | if (typeof callbackWay === 'function') callbackWay(p); 108 | break; 109 | case 'relation': 110 | if (!p.members) { 111 | console.log('Empty relation', p); 112 | break; 113 | } 114 | p.members.map((mem) => { 115 | mem.obj = (mem.type == 'way' ? ways : nodes)[mem.ref]; 116 | }); 117 | // p has type=relation, id, tags={k:v}, members=[{role, obj}] 118 | if (typeof callbackRelation === 'function') callbackRelation(p); 119 | break; 120 | } 121 | } 122 | if (typeof callbackDone === 'function') callbackDone(); 123 | }; 124 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | bracketSpacing: false, 4 | singleQuote: true 5 | }; 6 | -------------------------------------------------------------------------------- /railway.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Railway Map 5 | 6 | 7 | 8 | 9 | 10 | 167 | 168 | 190 | 191 | 192 |
193 | 194 | 195 | -------------------------------------------------------------------------------- /restaurant.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Restaurant Map 5 | 6 | 7 | 8 | 9 | 10 | 66 | 67 | 78 | 79 | 80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /semicircle.js: -------------------------------------------------------------------------------- 1 | L.SemicircleMarker = L.CircleMarker.extend({ 2 | options: { 3 | radius: 10, 4 | weight: 2, 5 | angle: 0, 6 | }, 7 | initialize(latlng, options) { 8 | L.CircleMarker.prototype.initialize.call(this, latlng, null, options); 9 | this._radius = this.options.radius; 10 | this._angle = this.options.angle; 11 | }, 12 | projectLatlngs() { 13 | this._point = this._map.latLngToLayerPoint(this._latlng); 14 | }, 15 | setRadius(radius) { 16 | this._radius = radius; 17 | return this.redraw(); 18 | }, 19 | setAngle(angle) { 20 | this._angle = angle; 21 | return this.redraw(); 22 | }, 23 | _updatePath() { 24 | var p = this._point; 25 | var r = this._radius; 26 | this._path.setAttribute('d', `M${p.x},${p.y - r}A${r},${r},0,1,1,${p.x},${p.y + r}`); 27 | this._path.setAttribute('transform', `rotate(${[this._angle + 90, p.x, p.y].join(',')})`); 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /wiwosm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WIWOSM 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 73 | 74 | 90 | 91 | 92 |
93 | 94 | 95 | --------------------------------------------------------------------------------