diff --git a/Makefile b/Makefile index 03bf5b253..4f4db2adf 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ DS_SCHEDULE=$(patsubst content/static/datenspuren/$(DS_YEAR)/fahrplan/schedule/% DATESTAMP=build/.stamp-$(shell date +%Y-%m-%d) # Dateinamen der zu erzeugenden html-Dateien: -CONTENT=$(PAGES) $(NEWS_PAGES) $(DS_PAGES) $(DS_FEEDS) $(NEWSFILES) build/calendar.html +CONTENT=$(PAGES) $(NEWS_PAGES) $(DS_PAGES) $(DS_FEEDS) $(NEWSFILES) build/calendar.html build/datenspuren/$(DS_YEAR)/pois.json # 'normale' Seiten: PAGES:=$(patsubst content/pages/%.xml, build/%.html, $(wildcard content/pages/*.xml)) @@ -305,6 +305,10 @@ build/datenspuren/2013/mitschnitte-rss.xml: content/news/ds13-videomitschnitte-k build/datenspuren/2014/mitschnitte-rss.xml: content/news/ds14-mitschnitte-online.xml $(STYLE) $(call xml_process) +build/datenspuren/$(DS_YEAR)/pois.json: + wget -O $@ --post-data="data=[out:json];($(foreach a, 'amenity'='restaurant' 'amenity'='fast_food' 'amenity'='cafe' 'amenity'='ice_cream' 'amenity'='bakery' 'shop'='convenience' 'shop'='supermarket', node(51.01,13.76,51.07,13.82)[${a}];));out;" http://overpass-api.de/api/interpreter + + xhtml5-validate: $(patsubst build/%.html, build/%.html.xhtml5-validate, $(CONTENT)) build/%.html.xhtml5-validate: build/%.html ./scripts/validate_xhtml5.sh $< diff --git a/content/static/datenspuren/2015/script/pois.js b/content/static/datenspuren/2015/script/pois.js new file mode 100644 index 000000000..cbe2dcedc --- /dev/null +++ b/content/static/datenspuren/2015/script/pois.js @@ -0,0 +1,141 @@ +var currentPosition = { + lat: 51.0420162, + lon: 13.7976983 +} +var geoListeners = [] + +function POI(info) { + this.info = info +} + +POI.prototype.getDistance = function() { + return WGS84Util.distanceBetween({ + coordinates: [this.info.lon, this.info.lat] + }, { + coordinates: [currentPosition.lon, currentPosition.lat] + }) +} + +POI.prototype.makeArrowEl = function() { + var el = $('

') + + var update = function() { + var bearing = -180.0 * Math.atan2( + this.info.lat - currentPosition.lat, + this.info.lon - currentPosition.lon + ) / Math.PI + el.find('.arrow span').css('transform', 'rotate(' + (bearing - (currentPosition.heading || 0)) + 'deg)') + if (typeof currentPosition.heading === 'number') + el.find('.arrow span').text("➡") + + el.find('.dist').text(formatDistance(this.getDistance())) + }.bind(this) + + // Initialize: + update() + // hook onGeo listener + geoListeners.push(update) + + return el +} + +function onGeo(geo) { + console.log("onGeo", geo) + currentPosition = { + lat: geo.coords.latitude, + lon: geo.coords.longitude, + heading: geo.heading + } + // call listeners + geoListeners.forEach(function(f) { + f(currentPosition) + }) +} + +function formatDistance(x) { + if (x >= 1000) { + return (Math.round(x / 100) / 10) + " km" + } else { + return Math.round(x) + " m" + } +} + +function makeLink(link) { + var a = $('') + a.attr('href', link.indexOf("://") < 0 ? "http://" + link : link) + a.text(link.replace(/^\w+?:\/\//, "").replace(/\/+$/, "")) + return a +} + +navigator.geolocation.getCurrentPosition(function(geo) { + onGeo(geo) + navigator.geolocation.watchPosition(onGeo, null, { + enableHighAccuracy: true + }) +}) + +var h3 = $('

Imbißmöglichkeiten anzeigen

') +$('article').append(h3) + +var ran = false +h3.click(function() { + if (ran) return + ran = true + + h3.addClass('expanded') + h3.text("Lade Imbißmöglichkeiten...") + + $.ajax({ + url: "pois.json", + success: function(json) { + var pois = json.elements.map(function(info) { + return new POI(info) + }).filter(function(poi) { + return poi.getDistance() <= 1000 + }).sort(function(a, b) { + return a.getDistance() - b.getDistance() + }) + + var dl = $('
') + pois.forEach(function(poi) { + if (!poi.info.tags.name) { + console.warn("No name:", poi.info) + return // Skip + } + + var dt = $('
') + dt.find('a'). + attr('href', "https://www.openstreetmap.org/" + poi.info.type + "/" + poi.info.id). + text(poi.info.tags.name) + + if (poi.info.tags['addr:street'] && poi.info.tags['addr:housenumber']) { + var addr = $('') + addr.text(poi.info.tags['addr:street'] + " " + + poi.info.tags['addr:housenumber']) + dt.append(addr) + } + if (poi.info.tags.website) { + var addr = $('') + addr.append(makeLink(poi.info.tags.website)) + dt.append(addr) + } + dt.prepend(poi.makeArrowEl()) + dl.append(dt) + + if (poi.info.tags.opening_hours) { + poi.info.tags.opening_hours.split(/\s*;\s*/g).forEach(function(range) { + var dd = $('
') + dd.text(range) + dl.append(dd) + }) + } + }) + $('article').append(dl) + h3.text("Imbißmöglichkeiten in der Umgebung") + $('article').append("

Quelle: OpenStreetMap. Stündlich aktualisiert mithilfe Overpass Turbo.

") + }, + error: function() { + h3.text("Fehler! Versuch z.B. osm24.eu stattdessen.") + } + }) +}) diff --git a/content/static/datenspuren/2015/script/wgs84util.js b/content/static/datenspuren/2015/script/wgs84util.js new file mode 100644 index 000000000..3a4eb0adf --- /dev/null +++ b/content/static/datenspuren/2015/script/wgs84util.js @@ -0,0 +1,251 @@ +/** @fileOverview Geographic coordinate utilities using WGS84 datum + * @author cs_brandt + * @date 02/25/2013 + */ + + +/** @module wgs84-util */ +var WGS84Util = window.WGS84Util = {}; + +// Semi-Major Axis (Equatorial Radius) +var SEMI_MAJOR_AXIS = 6378137.0; +// First Eccentricity Squared +var ECC_SQUARED = 0.006694380004260827; + +/** + * From: Haversine formula - RW Sinnott, "Virtues of the Haversine", + * Sky and Telescope, vol 68, no 2, 1984 + * + * @param {object} coordA GeoJSON point + * @param {object} coordB GeoJSON point + * @return {number} the distance from this point to the supplied point, in km + * (using Haversine formula) + * + */ +WGS84Util.distanceBetween = function(coordA, coordB) { + var lat1 = this.degToRad(coordA.coordinates[1]), lon1 = this.degToRad(coordA.coordinates[0]); + var lat2 = this.degToRad(coordB.coordinates[1]), lon2 = this.degToRad(coordB.coordinates[0]); + var dLat = lat2 - lat1; + var dLon = lon2 - lon1; + + var a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(lat1) * Math.cos(lat2) * + Math.sin(dLon/2) * Math.sin(dLon/2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + var d = SEMI_MAJOR_AXIS * c; + return d; +}; + +/** + * Returns the destination point from this point having travelled the given distance (in m) on the + * given initial bearing (bearing may vary before destination is reached) + * + * see http://williams.best.vwh.net/avform.htm#LL + * + * @param {object} coordA GeoJSON point + * @param {Number} brng: Initial bearing in degrees + * @param {Number} dist: Distance in m + * + * @returns {object} GeoJSON destination point + */ +WGS84Util.destinationPoint = function(coordA, brng, dist) { + dist = typeof(dist) == 'number' ? dist : typeof(dist) == 'string' && dist.trim() != '' ? +dist : NaN; + dist = dist / SEMI_MAJOR_AXIS; // convert dist to angular distance in radians + brng = this.degToRad(brng); // + var lat1 = this.degToRad(coordA.coordinates[1]), lon1 = this.degToRad(coordA.coordinates[0]); + + var lat2 = Math.asin( Math.sin(lat1) * Math.cos(dist) + + Math.cos(lat1) * Math.sin(dist) * Math.cos(brng) ); + var lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(dist) * Math.cos(lat1), + Math.cos(dist) - Math.sin(lat1) * Math.sin(lat2)); + lon2 = (lon2 + 3 * Math.PI) % (2 * Math.PI) - Math.PI; // normalise to -180..+180º + + return { + "type": "Point", + "coordinates": [parseFloat(this.radToDeg(lon2).toFixed(10)), parseFloat(this.radToDeg(lat2).toFixed(10))] + }; +}; + +/** + * Conversion from degrees to radians. + * + * @param {number} deg the angle in degrees. + * @return {number} the angle in radians. + */ +WGS84Util.degToRad = function(deg) { + return (deg * (Math.PI / 180.0)); +}; + +/** + * Conversion from radians to degrees. + * + * @param {number} rad the angle in radians. + * @return {number} the angle in degrees. + */ +WGS84Util.radToDeg = function(rad) { + return (180.0 * (rad / Math.PI)); +}; + +/** + * Converts a set of Longitude and Latitude co-ordinates to UTM + * using the WGS84 ellipsoid. + * + * @param {object} ll Object literal with lat and lon properties + * representing the WGS84 coordinate to be converted. + * @return {object} Object literal containing the UTM value with easting, + * northing, zoneNumber and zoneLetter properties, and an optional + * accuracy property in digits. Returns null if the conversion failed. + */ +WGS84Util.LLtoUTM = function(ll) { + var Lat = ll.coordinates[1]; + var Long = ll.coordinates[0]; + var k0 = 0.9996; + var LongOrigin; + var eccPrimeSquared; + var N, T, C, A, M; + var LatRad = this.degToRad(Lat); + var LongRad = this.degToRad(Long); + var LongOriginRad; + var ZoneNumber; + var zoneLetter = 'N'; + // (int) + ZoneNumber = Math.floor((Long + 180) / 6) + 1; + + //Make sure the longitude 180.00 is in Zone 60 + if (Long === 180) { + ZoneNumber = 60; + } + + // Special zone for Norway + if (Lat >= 56.0 && Lat < 64.0 && Long >= 3.0 && Long < 12.0) { + ZoneNumber = 32; + } + + // Special zones for Svalbard + if (Lat >= 72.0 && Lat < 84.0) { + if (Long >= 0.0 && Long < 9.0) { + ZoneNumber = 31; + } else if (Long >= 9.0 && Long < 21.0) { + ZoneNumber = 33; + } else if (Long >= 21.0 && Long < 33.0) { + ZoneNumber = 35; + } else if (Long >= 33.0 && Long < 42.0) { + ZoneNumber = 37; + } + } + + LongOrigin = (ZoneNumber - 1) * 6 - 180 + 3; //+3 puts origin + // in middle of + // zone + LongOriginRad = this.degToRad(LongOrigin); + + eccPrimeSquared = (ECC_SQUARED) / (1 - ECC_SQUARED); + + N = SEMI_MAJOR_AXIS / Math.sqrt(1 - ECC_SQUARED * Math.sin(LatRad) * Math.sin(LatRad)); + T = Math.tan(LatRad) * Math.tan(LatRad); + C = eccPrimeSquared * Math.cos(LatRad) * Math.cos(LatRad); + A = Math.cos(LatRad) * (LongRad - LongOriginRad); + + M = SEMI_MAJOR_AXIS * ((1 - ECC_SQUARED / 4 - 3 * ECC_SQUARED * ECC_SQUARED / 64 - 5 * ECC_SQUARED * ECC_SQUARED * ECC_SQUARED / 256) * LatRad - (3 * ECC_SQUARED / 8 + 3 * ECC_SQUARED * ECC_SQUARED / 32 + 45 * ECC_SQUARED * ECC_SQUARED * ECC_SQUARED / 1024) * Math.sin(2 * LatRad) + (15 * ECC_SQUARED * ECC_SQUARED / 256 + 45 * ECC_SQUARED * ECC_SQUARED * ECC_SQUARED / 1024) * Math.sin(4 * LatRad) - (35 * ECC_SQUARED * ECC_SQUARED * ECC_SQUARED / 3072) * Math.sin(6 * LatRad)); + + var UTMEasting = (k0 * N * (A + (1 - T + C) * A * A * A / 6.0 + (5 - 18 * T + T * T + 72 * C - 58 * eccPrimeSquared) * A * A * A * A * A / 120.0) + 500000.0); + + var UTMNorthing = (k0 * (M + N * Math.tan(LatRad) * (A * A / 2 + (5 - T + 9 * C + 4 * C * C) * A * A * A * A / 24.0 + (61 - 58 * T + T * T + 600 * C - 330 * eccPrimeSquared) * A * A * A * A * A * A / 720.0))); + + if (Lat < 0.0) { + UTMNorthing += 10000000.0; //10000000 meter offset for + // southern hemisphere + zoneLetter = 'S'; + } + + return {"type": "Feature", "geometry": {"type": "Point", "coordinates": [parseFloat(UTMEasting.toFixed(1)), parseFloat(UTMNorthing.toFixed(1))]}, "properties": {"zoneLetter": zoneLetter, "zoneNumber": ZoneNumber}}; +}; + +/** + * Converts UTM coords to lat/long, using the WGS84 ellipsoid. This is a convenience + * class where the Zone can be specified as a single string eg."60N" which + * is then broken down into the ZoneNumber and ZoneLetter. + * + * @param {object} utm An object literal with northing, easting, zoneNumber + * and zoneLetter properties. If an optional accuracy property is + * provided (in meters), a bounding box will be returned instead of + * latitude and longitude. + * @return {object} An object literal containing either lat and lon values + * (if no accuracy was provided), or top, right, bottom and left values + * for the bounding box calculated according to the provided accuracy. + * Returns null if the conversion failed. + */ +WGS84Util.UTMtoLL = function(utm) { + var UTMNorthing = utm.geometry.coordinates[1]; + var UTMEasting = utm.geometry.coordinates[0]; + var zoneLetter = utm.properties.zoneLetter; + var zoneNumber = utm.properties.zoneNumber; + // check the ZoneNummber is valid + if (zoneNumber < 0 || zoneNumber > 60) { + return null; + } + + var k0 = 0.9996; + var eccPrimeSquared; + var e1 = (1 - Math.sqrt(1 - ECC_SQUARED)) / (1 + Math.sqrt(1 - ECC_SQUARED)); + var N1, T1, C1, R1, D, M; + var LongOrigin; + var mu, phi1Rad; + + // remove 500,000 meter offset for longitude + var x = UTMEasting - 500000.0; + var y = UTMNorthing; + + // We must know somehow if we are in the Northern or Southern + // hemisphere, this is the only time we use the letter So even + // if the Zone letter isn't exactly correct it should indicate + // the hemisphere correctly + if (zoneLetter === 'S') { + y -= 10000000.0; // remove 10,000,000 meter offset used + // for southern hemisphere + } + + // There are 60 zones with zone 1 being at West -180 to -174 + LongOrigin = (zoneNumber - 1) * 6 - 180 + 3; // +3 puts origin + // in middle of + // zone + eccPrimeSquared = (ECC_SQUARED) / (1 - ECC_SQUARED); + + M = y / k0; + mu = M / (SEMI_MAJOR_AXIS * (1 - ECC_SQUARED / 4 - 3 * ECC_SQUARED * ECC_SQUARED / 64 - 5 * ECC_SQUARED * ECC_SQUARED * ECC_SQUARED / 256)); + + phi1Rad = mu + (3 * e1 / 2 - 27 * e1 * e1 * e1 / 32) * Math.sin(2 * mu) + (21 * e1 * e1 / 16 - 55 * e1 * e1 * e1 * e1 / 32) * Math.sin(4 * mu) + (151 * e1 * e1 * e1 / 96) * Math.sin(6 * mu); + // double phi1 = ProjMath.radToDeg(phi1Rad); + N1 = SEMI_MAJOR_AXIS / Math.sqrt(1 - ECC_SQUARED * Math.sin(phi1Rad) * Math.sin(phi1Rad)); + T1 = Math.tan(phi1Rad) * Math.tan(phi1Rad); + C1 = eccPrimeSquared * Math.cos(phi1Rad) * Math.cos(phi1Rad); + R1 = SEMI_MAJOR_AXIS * (1 - ECC_SQUARED) / Math.pow(1 - ECC_SQUARED * Math.sin(phi1Rad) * Math.sin(phi1Rad), 1.5); + D = x / (N1 * k0); + + var lat = phi1Rad - (N1 * Math.tan(phi1Rad) / R1) * (D * D / 2 - (5 + 3 * T1 + 10 * C1 - 4 * C1 * C1 - 9 * eccPrimeSquared) * D * D * D * D / 24 + (61 + 90 * T1 + 298 * C1 + 45 * T1 * T1 - 252 * eccPrimeSquared - 3 * C1 * C1) * D * D * D * D * D * D / 720); + lat = this.radToDeg(lat); + + var lon = (D - (1 + 2 * T1 + C1) * D * D * D / 6 + (5 - 2 * C1 + 28 * T1 - 3 * C1 * C1 + 8 * eccPrimeSquared + 24 * T1 * T1) * D * D * D * D * D / 120) / Math.cos(phi1Rad); + lon = LongOrigin + this.radToDeg(lon); + + var result = { "type": "Point", "coordinates": [] }; + if (utm.accuracy) { + var topRight = this.UTMtoLL({ + northing: utm.northing + utm.accuracy, + easting: utm.easting + utm.accuracy, + zoneLetter: utm.zoneLetter, + zoneNumber: utm.zoneNumber + }); + result = { + top: topRight.lat, + right: topRight.lon, + bottom: lat, + left: lon + }; + } else { + result.coordinates[0] = parseFloat(lon.toFixed(8)); + result.coordinates[1] = parseFloat(lat.toFixed(8)); + } + + return result; +}; diff --git a/content/static/datenspuren/2015/style/style.css b/content/static/datenspuren/2015/style/style.css index 7312debb0..12e3a94c6 100644 --- a/content/static/datenspuren/2015/style/style.css +++ b/content/static/datenspuren/2015/style/style.css @@ -130,3 +130,48 @@ footer a { display: inline; padding: 0 0.3em; } + +#foodlocator { + cursor: pointer; +} +#foordlocator.expanded { + cursor: default; +} + +#food dt { + clear: both; + margin-top: 0.5em; + font-weight: bold; +} +#food dt .addr, #food dt .website { + font-weight: normal; + margin-left: 0.8em; + color: #444; +} +#food dd { + color: #444; + font-size: 80%; +} + +.geo-arrow { + float: left; + clear: left; + margin: 0 0.5em 1.5em 0; +} +.geo-arrow .arrow { + margin: 0; + text-align: center; + font-size: 200%; + font-weight: bold; + line-height: 1em; +} +.geo-arrow .arrow span { + display: inline-block; +} +.geo-arrow .dist { + margin: 0; + text-align: center; + font-size: 70%; + line-height: 1em; + font-weight: 100; +} diff --git a/xsl/datenspuren/xhtml5.xsl b/xsl/datenspuren/xhtml5.xsl index bd0e2229f..6d7cad3a9 100644 --- a/xsl/datenspuren/xhtml5.xsl +++ b/xsl/datenspuren/xhtml5.xsl @@ -105,6 +105,16 @@ responsive: true }); + + + +