├── .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 |
--------------------------------------------------------------------------------