Difference between revisions of "Widget:SpaceAPI"
m (added follow toggle machanism) |
m (Try and fix SpaceAPI popup alignment) |
||
(58 intermediate revisions by the same user not shown) | |||
Line 10: | Line 10: | ||
|url=/spaceAPI/ | |url=/spaceAPI/ | ||
|width=260px | |width=260px | ||
− | |height= | + | |height=300px |
|padding=8px | |padding=8px | ||
|interval=20 | |interval=20 | ||
|float=right | |float=right | ||
+ | |features=beacon,annex | ||
}}</nowiki> | }}</nowiki> | ||
This will give the following result:<br/> | This will give the following result:<br/> | ||
{{#widget:{{PAGENAME}} | {{#widget:{{PAGENAME}} | ||
− | |url=/spaceAPI/ | + | |url=/spaceAPI/?beacon_log=HoT |
|width=260px | |width=260px | ||
− | |height= | + | |height=300px |
|padding=8px | |padding=8px | ||
|interval=20 | |interval=20 | ||
|float=right | |float=right | ||
+ | |features=beacon,annex | ||
}}<br/> | }}<br/> | ||
'''Notes''' | '''Notes''' | ||
Line 36: | Line 38: | ||
(function( ) | (function( ) | ||
{ | { | ||
− | + | "use strict"; | |
− | + | /* | |
− | + | TODO: | |
− | + | separate rooms (get latlng) | |
− | + | zoom onto toolbox | |
− | + | table island not correct (4, not 5 tables) | |
− | + | ||
− | + | */ | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | if ( typeof SpaceAPI === "undefined" ) | |
− | + | { | |
− | + | window.SpaceAPI = function( _width, _height, _float, _padding, _url, _interval, _features ) | |
− | + | { | |
− | + | this._width = _width; | |
− | + | this._height = _height; | |
− | + | this._padding = _padding; | |
− | + | this._url = _url; | |
− | + | this._interval = 1000 * _interval; | |
− | + | this._float = _float; | |
− | + | this._features = _features; | |
− | + | } | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | SpaceAPI.prototype.data = null; | |
− | + | SpaceAPI.prototype._width = null; | |
− | + | SpaceAPI.prototype._height = null; | |
− | + | SpaceAPI.prototype._float = null; | |
− | + | SpaceAPI.prototype._url = null; | |
− | + | SpaceAPI.prototype._features = null; | |
+ | SpaceAPI.prototype._interval = null; | ||
+ | SpaceAPI.prototype._intervalId = null; | ||
+ | SpaceAPI.prototype._node = null; | ||
+ | SpaceAPI.prototype._leaflet = null; | ||
+ | SpaceAPI.prototype._msgLoading = "Loading.."; | ||
+ | SpaceAPI.prototype._msgError = "Error"; | ||
+ | SpaceAPI.prototype._msgParserError = "Failed to parse space state information"; | ||
+ | SpaceAPI.prototype._msgOpen = "Open"; | ||
+ | SpaceAPI.prototype._msgClosed = "Closed"; | ||
+ | SpaceAPI.prototype._msgUnknown = "Unknown"; | ||
+ | SpaceAPI.prototype._msgSince = "Since: "; | ||
+ | SpaceAPI.prototype._colorOpen = "#0f0"; | ||
+ | SpaceAPI.prototype._colorClosed = "#f00"; | ||
+ | SpaceAPI.prototype._colorUnknown = "#f70"; | ||
+ | SpaceAPI.prototype._debug = null; | ||
− | + | SpaceAPI.prototype._drawBeaconPolyLine = function( ) | |
− | + | { | |
− | + | if ( !this.data.sensors || !this.data.sensors.beacon || this.data.sensors.beacon.length < 50 ) | |
− | + | return; | |
− | + | var points = []; | |
− | + | var bounds = this._leaflet.map.getBounds(); | |
− | |||
− | |||
− | |||
− | |||
− | + | // Only add points within the bounds | |
− | + | if ( this._leaflet.beaconCount !== this.data.sensors.beacon.length ) | |
− | + | { | |
− | + | this._leaflet.beaconCount = this.data.sensors.beacon.length; | |
− | + | for ( var n = 0, len = this.data.sensors.beacon.length; n < len; ++n ) | |
− | + | { | |
− | + | var point = L.latLng( this.data.sensors.beacon[n].location.lat, this.data.sensors.beacon[n].location.lon ); | |
− | + | points.push( point ); | |
− | + | } | |
− | |||
− | |||
− | + | if ( !this._leaflet.beaconline ) | |
− | + | this._leaflet.beaconline = L.polyline(points, {color: 'green'}).addTo( this._leaflet.map ); | |
− | + | else | |
− | + | this._leaflet.beaconline.setLatLngs( points ); | |
− | + | } | |
− | + | }; | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | SpaceAPI.prototype.start = function( ) | |
− | + | { | |
− | + | // Use interval timer id as image id | |
+ | this._debug = ( location.hash.split("#").slice(1).indexOf("debug") !== -1 ); | ||
− | + | this._intervalId = 0; | |
− | + | if ( this._interval > 0 ) | |
− | + | this._intervalId = setInterval( this._fetchState.bind( this ), this._interval ); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | document.write( '<div id="spaceAPI' + this._intervalId + '"></div>' ); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | var node = document.getElementById( "spaceAPI" + this._intervalId ); | |
− | + | if ( !node ) | |
− | + | { | |
+ | console && console.log( "node not found" ); | ||
+ | return; | ||
+ | } | ||
+ | node.style.width = this._width; | ||
+ | node.style.height = this._height; | ||
− | + | node.style.textAlign = "center"; | |
− | + | node.style.BoxShadow = "3px 3px 4px rgba(0,0,0,0.2)"; | |
− | + | node.style.position = "relative"; | |
− | + | if ( this._float ) | |
− | + | node.style.float = this._float; | |
− | |||
− | + | this._node = node.appendChild( document.createElement( "a" ) ); | |
− | + | this._node.style.display = "block"; | |
− | + | this._node.style.color = "inherit"; | |
− | + | this._node.style.padding = this._padding; | |
− | + | this._node.textContent = this._msgLoading; | |
− | + | this._node.href = "/spaceAPI/statechanges.html"; | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | if ( this._features.split(",").indexOf( "beacon" ) >= 0 ) | |
+ | { | ||
+ | var srcNode; | ||
+ | var mapNode = node.appendChild( document.createElement( "div" ) ); | ||
+ | mapNode.style.width = "100%"; | ||
+ | mapNode.style.height = "calc(" + this._height + " + " + this._padding + " - 2em)"; | ||
+ | |||
+ | srcNode = document.createElement( "link" ); | ||
+ | srcNode.href = "/leaflet/leaflet.css"; | ||
+ | srcNode.rel = "stylesheet"; | ||
+ | srcNode.type = "text/css"; | ||
− | + | // Load the css | |
− | + | ( document.head || document.documentElement ).appendChild( srcNode ); | |
− | + | ||
+ | srcNode = document.createElement( "script" ); | ||
+ | srcNode.src = "/leaflet/leaflet.js"; | ||
+ | srcNode.type = "text/javascript"; | ||
+ | srcNode.addEventListener( "load", function( _evt ) | ||
+ | { | ||
+ | this._leaflet = {}; | ||
+ | this._leaflet.point = L.latLng( 50.8925,5.9713 ); | ||
+ | this._leaflet.map = L.map( mapNode ).panTo( this._leaflet.point ); | ||
+ | //this._leaflet.map = L.map( mapNode ).setView( this._leaflet.point, 16); | ||
− | + | L.CRS.CustomZoom = L.extend({}, L.CRS.EPSG3857, | |
− | + | { | |
− | + | scale: function( zoom ) { | |
+ | if ( zoom < 24 ) | ||
+ | return 256 * Math.pow( 2, zoom ); | ||
− | + | // Freeze the actual zooming above this level to show the different floors | |
− | + | // 256 * pow( 2, 23 ) | |
− | + | return 2147483648; | |
− | + | } | |
− | + | }); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | /**********************************/ | |
− | + | L.TileLayer.Functional = L.TileLayer.extend({ | |
− | |||
− | |||
− | |||
− | + | _tileFunction: null, | |
− | |||
− | |||
− | |||
− | |||
− | + | initialize: function (tileFunction, options) { | |
− | + | this._tileFunction = tileFunction; | |
− | + | L.TileLayer.prototype.initialize.call(this, null, options); | |
− | + | }, | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | getTileUrl: function (tilePoint) { | |
− | + | var map = this._map, | |
− | + | crs = map.options.crs, | |
− | + | tileSize = this.options.tileSize, | |
− | + | zoom = tilePoint.z, | |
− | + | nwPoint = tilePoint.multiplyBy(tileSize), | |
+ | sePoint = nwPoint.add(new L.Point(tileSize, tileSize)), | ||
+ | nw = crs.project(map.unproject(nwPoint, zoom)), | ||
+ | se = crs.project(map.unproject(sePoint, zoom)), | ||
+ | bbox = [nw.x, se.y, se.x, nw.y].join(','); | ||
− | + | // Setup object to send to tile function. | |
− | + | var view = { | |
− | + | bbox: bbox, | |
− | + | width: tileSize, | |
− | + | height: tileSize, | |
− | + | zoom: zoom, | |
− | + | tile: { | |
+ | row: this.options.tms ? this._tileNumBounds.max.y - tilePoint.y : tilePoint.y, | ||
+ | column: tilePoint.x | ||
+ | }, | ||
+ | subdomain: this._getSubdomain(tilePoint) | ||
+ | }; | ||
− | + | return this._tileFunction(view); | |
− | + | }, | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | _loadTile: function (tile, tilePoint) { | |
+ | tile._layer = this; | ||
+ | tile.onload = this._tileOnLoad; | ||
+ | tile.onerror = this._tileOnError; | ||
− | + | this._adjustTilePoint(tilePoint); | |
− | + | var tileUrl = this.getTileUrl(tilePoint); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | if (typeof tileUrl === 'string') { | |
− | + | tile.src = tileUrl; | |
− | + | this.fire('tileloadstart', { | |
− | + | tile: tile, | |
− | + | url: tile.src | |
− | + | }); | |
+ | } else if (typeof tileUrl.then === 'function') { | ||
+ | // Assume we are dealing with a promise. | ||
+ | var self = this; | ||
+ | tileUrl.then(function (tileUrl) { | ||
+ | tile.src = tileUrl; | ||
+ | self.fire('tileloadstart', { | ||
+ | tile: tile, | ||
+ | url: tile.src | ||
+ | }); | ||
+ | }); | ||
+ | } | ||
+ | } | ||
+ | }); | ||
− | + | L.tileLayer.functional = function (tileFunction, options) { | |
− | + | return new L.TileLayer.Functional(tileFunction, options); | |
− | + | }; | |
− | + | /*******************************/ | |
− | |||
− | |||
− | + | L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { | |
− | + | attribution: '© <a href="//openstreetmap.org/copyright">OpenStreetMap</a> contributors', | |
+ | minZoom: 2, | ||
+ | maxZoom: 28, | ||
+ | maxNativeZoom: 19 | ||
+ | }).addTo( this._leaflet.map ); | ||
+ | |||
+ | // Generic zoom (note that this will cause 404s | ||
+ | L.tileLayer("/images/ACK_{x}_{y}_{z}.png", { | ||
+ | attribution: 'ACKspace', | ||
+ | minZoom: 24, | ||
+ | maxZoom: 28, | ||
+ | maxNativeZoom: 23 | ||
+ | }).addTo( this._leaflet.map ); | ||
+ | |||
+ | // special zoom | ||
+ | var funcLayer = new L.TileLayer.Functional( function( view ) | ||
+ | { | ||
+ | var bounds = { | ||
+ | 16 : { cl: 33855, ch: 33855, rl: 21971, rh: 21971 }, | ||
+ | 17 : { cl: 67710, ch: 67710, rl: 43942, rh: 43942 }, | ||
+ | 18 : { cl: 135420, ch: 135420, rl: 87884, rh: 87884 }, | ||
+ | 19 : { cl: 270840, ch: 270840, rl: 175768, rh: 175768 }, | ||
+ | 20 : { cl: 541680, ch: 541680, rl: 351536, rh: 351537}, | ||
+ | 21 : { cl: 1083360, ch: 1083361, rl: 703073, rh: 703074 }, | ||
+ | 22 : { cl: 2166720, ch: 2166723, rl: 1406146, rh: 1406148 }, | ||
+ | 23 : { cl: 4333441, ch: 4333446, rl: 2812292, rh: 2812296 }, | ||
+ | 24 : { cl: 8666890, ch: 8666890, rl: 5624590, rh: 5624590 }, | ||
+ | 25 : { cl: 17333781, ch: 17333781, rl: 11249181, rh: 11249181 }, | ||
+ | 26 : { cl: 34667562, ch: 34667562, rl: 22498362, rh: 22498362 }, | ||
+ | 27 : { cl: 69335125, ch: 69335125, rl: 44996725, rh: 44996725 }, | ||
+ | 28 : { cl: 138670250, ch: 138670251, rl: 89993450, rh: 89993451 } | ||
+ | } | ||
+ | if ( bounds[ view.zoom ] ) | ||
+ | { | ||
+ | if ( view.tile.column < bounds[ view.zoom ].cl || view.tile.column > bounds[ view.zoom ].ch ) | ||
+ | return "/images/blank.png"; | ||
+ | if ( view.tile.row < bounds[ view.zoom ].rl || view.tile.row > bounds[ view.zoom ].rh ) | ||
+ | return "/images/blank.png"; | ||
+ | } | ||
+ | |||
+ | var url = "/images/ACK_{x}_{y}_{z}.png" | ||
+ | .replace('{z}', view.zoom) | ||
+ | .replace('{y}', view.tile.row) | ||
+ | .replace('{x}', view.tile.column) | ||
+ | .replace('{s}', view.subdomain); | ||
+ | |||
+ | return url; | ||
+ | }, | ||
+ | { | ||
+ | attribution: 'ACKspace', | ||
+ | minZoom: 16, | ||
+ | maxZoom: 28 | ||
+ | } ).addTo( this._leaflet.map ); | ||
+ | |||
+ | this._leaflet.map.on('moveend', function( _event ) | ||
+ | { | ||
+ | //this._drawBeaconPolyLine( _event ); | ||
+ | |||
+ | }, this ); | ||
+ | |||
+ | this._leaflet.map.on('zoomend', function( _event ) | ||
+ | { | ||
+ | var o = 1, z = _event.target.getZoom(); | ||
+ | |||
+ | /* | ||
+ | // Infinite zoom | ||
+ | if ( z > 27 ) | ||
+ | { | ||
+ | // boundingbox contains center and zoom > 27 | ||
+ | // then zoom out | ||
+ | // setView/panTo/setZoom/fitBounds/etc. | ||
+ | //_event.target.getCenter() | ||
+ | _event.target.setZoom( 2, true ); | ||
+ | } | ||
+ | */ | ||
+ | |||
+ | |||
+ | //this._drawBeaconPolyLine( _event ); | ||
+ | |||
+ | |||
+ | if ( !this._leaflet.temperatures["000005667ABD"] ) | ||
+ | return; | ||
+ | |||
+ | // zoom 18->23 opacity 1->0 | ||
+ | if ( z > 17 ) | ||
+ | o = Math.max( (23-z) / 5, 0 ); | ||
+ | this._leaflet.temperatures["000005667ABD"].setStyle( { fillOpacity: o } ); | ||
+ | }, this ); | ||
+ | |||
+ | if ( this._debug ) | ||
+ | { | ||
+ | this._leaflet.map.on( "click", function( _e ) | ||
+ | { | ||
+ | console.log( _e.latlng ); | ||
+ | }); | ||
+ | |||
+ | L.GridLayer.DebugCoords = L.GridLayer.extend({ | ||
+ | createTile: function (coords) { | ||
+ | var tile = document.createElement('div'); | ||
+ | tile.innerHTML = [coords.x, coords.y, coords.z].join(', '); | ||
+ | tile.style.outline = '1px solid red'; | ||
+ | return tile; | ||
+ | } | ||
+ | }); | ||
+ | |||
+ | L.gridLayer.debugCoords = function(opts) { | ||
+ | return new L.GridLayer.DebugCoords(opts); | ||
+ | }; | ||
+ | |||
+ | this._leaflet.map.addLayer( L.gridLayer.debugCoords() ); | ||
+ | } | ||
+ | |||
+ | // "Follow" control | ||
+ | this._leaflet.follow = null; | ||
+ | L.Control.Command = L.Control.extend( | ||
+ | { | ||
+ | options: | ||
+ | { | ||
+ | position: 'topleft', | ||
+ | }, | ||
+ | |||
+ | onAdd: function( _map ) | ||
+ | { | ||
+ | var controlDiv = L.DomUtil.create( "div", "leaflet-bar" ); | ||
+ | var controlUI = L.DomUtil.create( "a", "leaflet-clickable" + (this._leaflet.follow ? " toggle" : ""), controlDiv ); | ||
+ | controlUI.innerHTML = "⌖"; | ||
+ | controlUI.style.fontSize = "35px"; | ||
+ | controlUI.title = 'Follow beacon'; | ||
+ | |||
+ | L.DomEvent.addListener( controlUI, 'click', function( _evt ) | ||
+ | { | ||
+ | this._leaflet.follow = !this._leaflet.follow; | ||
+ | _evt.currentTarget.className = "leaflet-clickable" + (this._leaflet.follow ? " toggle" : ""); | ||
+ | |||
+ | // Update the map immediately | ||
+ | if ( this._leaflet.follow ) | ||
+ | { | ||
+ | // Determine the bounding box to 'follow | ||
+ | var bounds = L.latLngBounds( this._leaflet.beacons.map( function( _beacon ) | ||
+ | { | ||
+ | return _beacon.point; | ||
+ | } ) ); | ||
+ | |||
+ | if ( !this._leaflet.beacons.length ) | ||
+ | bounds.extend( this._leaflet.point ); | ||
+ | |||
+ | // Depending on polyline: don't zoom | ||
+ | //this._leaflet.map.fitBounds( bounds ); | ||
+ | //if ( this._leaflet.map.getZoom() > 18 ) | ||
+ | //this._leaflet.map.setZoom( 18 ); | ||
+ | } | ||
+ | }.bind( this ) ); | ||
+ | |||
+ | return controlDiv; | ||
+ | }.bind( this ) | ||
+ | } ); | ||
+ | |||
+ | this._leaflet.map.addControl( new L.Control.Command() ); | ||
+ | |||
+ | // Icons | ||
+ | this._leaflet.icons = { | ||
+ | "HoaB" : L.icon( { | ||
+ | iconUrl: '//maps.google.com/intl/en_us/mapfiles/ms/micons/cycling.png', | ||
+ | iconSize: [32, 32], | ||
+ | iconAnchor: [16, 26], | ||
+ | popupAnchor: [0, -26], | ||
+ | shadowUrl: '//maps.google.com/intl/en_us/mapfiles/ms/micons/cycling.shadow.png', | ||
+ | shadowSize: [59, 32], | ||
+ | shadowAnchor: [16, 26] | ||
+ | } ), | ||
+ | "HoT" : L.icon( { | ||
+ | iconUrl: '//maps.google.com/intl/en_us/mapfiles/ms/micons/bus.png', | ||
+ | iconSize: [32, 32], | ||
+ | iconAnchor: [16, 26], | ||
+ | popupAnchor: [0, -26], | ||
+ | shadowUrl: '//maps.google.com/intl/en_us/mapfiles/ms/micons/bus.shadow.png', | ||
+ | shadowSize: [59, 32], | ||
+ | shadowAnchor: [16, 26] | ||
+ | } ) | ||
+ | }; | ||
+ | this._leaflet.descriptions = { | ||
+ | "HoaB" : "Hackers on a Bike", | ||
+ | "HoT" : "Hackers on Tour" | ||
+ | }; | ||
+ | |||
+ | this._leaflet.marker = L.marker( this._leaflet.point ).addTo( this._leaflet.map ); | ||
+ | this._leaflet.beacons = []; | ||
+ | if ( this._debug ) | ||
+ | this._leaflet.temperatures = {}; | ||
+ | else | ||
+ | this._leaflet.temperatures = { | ||
+ | // outside | ||
+ | "000005671715" : L.polygon([[50.892537811238,5.9711101056338], [50.892517508729,5.9710966945888], [50.892534427487,5.9710564614536], [50.892715457818,5.9712093473674], [50.892696847256,5.9712790848018], [50.892808510518,5.9713836909534], [50.892788208127,5.9714373351337], [50.892774673195,5.9714319707157], [50.892681620427,5.971697509408], [50.892641015525,5.9716653228998], [50.892559805614,5.971893310666], [50.892569956861,5.9719120861291], [50.892559805614,5.9719496370554], [50.892377082796,5.9717940689325], [50.892399077248,5.9717189670801], [50.89233140198,5.9716545940637], [50.892350012689,5.9716009498835], [50.892326326331,5.9715794922114], [50.892414304169,5.9713032246828], [50.892458293026,5.971340775609]], {stroke:0,fillOpacity:0.8} ).addTo( this._leaflet.map ), | ||
+ | // cold zone | ||
+ | "000005667ABD" : L.polygon( [[50.892458293019,5.971340775608], [50.892410920402,5.9713032246818], [50.8924743319376, 5.9711186739150435], [50.89251872057557, 5.97115655487869]], {stroke:0,fillOpacity:0.8} ).addTo( this._leaflet.map ), | ||
+ | // barbecue | ||
+ | "DEADBEEF0" : L.circle( [ 50.89277, 5.97134 ], 1, {stroke:0,fillOpacity:0.8} ).addTo( this._leaflet.map ), | ||
+ | // hot zone | ||
+ | "00000567138A" : L.circle( [ 50.89250, 5.9711867 ], 2, {stroke:0,fillOpacity:0.8} ).addTo( this._leaflet.map ) | ||
+ | }; | ||
+ | |||
+ | var linesRed = [[ | ||
+ | [50.892514028030334, 5.971161649524675], [50.89251319586179, 5.971160894608546]],[ | ||
+ | [50.89251114428003, 5.9711696663976], [50.89251024343285, 5.9711689967662105]],[ | ||
+ | [50.8924980950481, 5.971208224073053], [50.892496785531904, 5.971207094194142]],[ | ||
+ | [50.892495831897186, 5.971215394539513], [50.89249461680935, 5.971214339470522]],[ | ||
+ | [50.892482398287335, 5.971254612622375], [50.89248148152484, 5.971253695897759]],[ | ||
+ | [50.89248006555828, 5.971261391486564], [50.8924792609356, 5.971260443329812]],[ | ||
+ | [50.89246652683481, 5.971301496043679], [50.8924656730421, 5.971300341188908]],[ | ||
+ | [50.892464393586884, 5.971307247454547], [50.89246356324314, 5.971306176293467]] | ||
+ | ]; | ||
+ | |||
+ | var linesBlue = [[ | ||
+ | [50.892505941019564, 5.971185087508389], [50.89250524711004, 5.971184503287078]],[ | ||
+ | [50.892503858003316, 5.971191544085743], [50.892503132264245, 5.9711907897144565]],[ | ||
+ | [50.89249048160161, 5.971230519935489], [50.89248932268764, 5.971229581079457]],[ | ||
+ | [50.89248770274671, 5.971239335350448], [50.89248661219504, 5.97123836979705]],[ | ||
+ | [50.89247468364275, 5.9712776963039005], [50.892473913492424, 5.97127688527138]],[ | ||
+ | [50.892472469662096, 5.971284294786302], [50.89247146830519, 5.971283237998365]],[ | ||
+ | [50.89245897037926, 5.971323475241662], [50.89245824463948, 5.9713221760466695]],[ | ||
+ | [50.89245672874569, 5.971331032450281], [50.89245581256435, 5.971329635940493]] | ||
+ | ]; | ||
+ | |||
+ | if ( this._debug ) | ||
+ | { | ||
+ | L.polyline(linesRed, {color: 'red'}).addTo( this._leaflet.map ); | ||
+ | L.polyline(linesBlue, {color: 'blue'}).addTo( this._leaflet.map ); | ||
+ | } | ||
+ | }.bind( this ) ); | ||
+ | |||
+ | // Load the script | ||
+ | ( document.head || document.documentElement ).appendChild( srcNode ); | ||
+ | } // beacon | ||
+ | |||
+ | // Update the space state immediately | ||
+ | setTimeout( this._fetchState.bind( this ), 1 ); | ||
+ | }; | ||
+ | SpaceAPI.prototype._determineColor = function( _temperature ) | ||
+ | { | ||
+ | var tempteratureColors = [ | ||
+ | [ -10, 0, 0, 0], // black | ||
+ | [ 0, 0, 0,255], // blue | ||
+ | [ 15, 255,255, 0], // yellow | ||
+ | [ 35, 255, 0, 0], // red | ||
+ | [ 45, 255,255,255], // white | ||
+ | [5000, 255,255,255] // white, blink | ||
+ | ]; | ||
+ | var index; | ||
+ | var ratio; | ||
+ | |||
+ | for ( var nTemp = tempteratureColors.length - 1; nTemp; nTemp-- ) | ||
+ | { | ||
+ | if ( _temperature >= tempteratureColors[ nTemp ][0] ) | ||
+ | break; | ||
+ | |||
+ | ratio = 0; | ||
+ | if ( index = nTemp ) | ||
+ | ratio = (_temperature - tempteratureColors[ nTemp - 1 ][0]) / ( tempteratureColors[ nTemp ][0] - tempteratureColors[ nTemp - 1 ][0] ); | ||
+ | } | ||
+ | |||
+ | var lo = tempteratureColors[ index ? index - 1 : 0 ]; | ||
+ | var hi = tempteratureColors[ index ]; | ||
+ | |||
+ | return "rgb("+Math.round( (lo[1] * (1 - ratio) + hi[1] * ratio) ) +","+Math.round( (lo[2] * (1 - ratio) + hi[2] * ratio))+","+Math.round( (lo[3] * (1 - ratio) + hi[3] * ratio))+")"; | ||
+ | }; | ||
+ | |||
+ | SpaceAPI.prototype._nlsTime = function( _time ) | ||
+ | { | ||
+ | var postfix; | ||
+ | if ( _time < 2 ) | ||
+ | { | ||
+ | return "moments"; | ||
+ | } | ||
+ | |||
+ | if ( _time > 31556952 ) | ||
+ | { | ||
+ | _time /= 31556952; | ||
+ | postfix = "Year"; | ||
+ | } | ||
+ | else if ( _time > 2629746 ) | ||
+ | { | ||
+ | _time /= 2629746; | ||
+ | postfix = "Month"; | ||
+ | } | ||
+ | else if ( _time > 604800 ) | ||
+ | { | ||
+ | _time /= 604800; | ||
+ | postfix = "Week"; | ||
+ | } | ||
+ | else if ( _time > 86400 ) | ||
+ | { | ||
+ | _time /= 86400; | ||
+ | postfix = "day"; | ||
+ | } | ||
+ | else if ( _time > 3600 ) | ||
+ | { | ||
+ | _time /= 3600; | ||
+ | postfix = "hour"; | ||
+ | } | ||
+ | else if ( _time > 60 ) | ||
+ | { | ||
+ | _time /= 60; | ||
+ | postfix = "minute"; | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | postfix = "second"; | ||
+ | } | ||
+ | |||
+ | _time = Math.round( _time ); | ||
+ | return _time + " " + postfix + (_time !== 1 ? "s" : ""); | ||
+ | }; | ||
+ | |||
+ | SpaceAPI.prototype._fetchState = function( ) | ||
+ | { | ||
+ | this._node.className = "processing"; | ||
+ | var xhr = new XMLHttpRequest( ); | ||
+ | if ( !!( "onload" in xhr ) ) | ||
+ | { | ||
+ | xhr.onreadystatechange = function( _event ) | ||
+ | { | ||
+ | if ( _event.target.readyState !== 4 ) | ||
+ | return; | ||
+ | if ( _event.target.status === 200 ) | ||
+ | this._xhr_onload.apply( this, arguments ); | ||
+ | else | ||
+ | this._xhr_onerror.apply( this, arguments ); | ||
+ | }.bind( this ); | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | // Modern xhr | ||
+ | xhr.onload = this._xhr_onload.bind( this ); | ||
+ | xhr.onerror = this._xhr_onerror.bind( this ); | ||
+ | } | ||
+ | |||
+ | xhr.open( "GET", this._url, true ); | ||
+ | |||
+ | // Tells server that this call is made for ajax purposes. | ||
+ | // Most libraries like jQuery/Prototype/Dojo do this | ||
+ | xhr.setRequestHeader( "X-Requested-With", "XMLHttpRequest" ); | ||
+ | |||
+ | // No data needs to be sent along with the request. | ||
+ | xhr.send( null ); | ||
+ | }; | ||
+ | |||
+ | SpaceAPI.prototype._updateState = function( _message, _color, _title ) | ||
+ | { | ||
+ | this._node.className = ""; | ||
+ | this._node.textContent = _message; | ||
+ | this._node.style.backgroundColor = _color; | ||
+ | if ( _title ) | ||
+ | this._node.title = _title; | ||
+ | else | ||
+ | this._node.title = ""; | ||
+ | }; | ||
+ | |||
+ | SpaceAPI.prototype._xhr_onload = function( _event ) | ||
+ | { | ||
+ | var open = null; | ||
+ | var message = null; | ||
+ | var title = null; | ||
+ | |||
+ | try | ||
+ | { | ||
+ | this.data = JSON.parse( _event.target.responseText ); | ||
+ | open = this.data.state.open; | ||
+ | message = this.data.state.message; | ||
+ | |||
+ | // Start as epoch timestamp (NOTE: check if timezone doesn't mess things up) | ||
+ | var d = new Date( 0 ); | ||
+ | d.setUTCSeconds( this.data.state.lastchange ); | ||
+ | title = this._msgSince + d.toLocaleString( ); | ||
+ | } | ||
+ | catch( _e ) | ||
+ | { | ||
+ | message = this._msgParserError; | ||
+ | } | ||
+ | |||
+ | if ( open ) | ||
+ | this._updateState( message || this._msgOpen, this._colorOpen, title ); | ||
+ | else if ( open === false ) | ||
+ | this._updateState( message || this._msgClosed, this._colorClosed, title ); | ||
+ | else | ||
+ | this._updateState( message || this._msgUnknown, this._colorUnknown, title ); | ||
+ | |||
+ | if ( this._leaflet ) | ||
+ | { | ||
+ | // Handle temperatures | ||
+ | if ( this.data.sensors && this.data.sensors.temperature && this.data.sensors.temperature.length ) | ||
+ | { | ||
+ | // Iterate the sensors and match a local sensor name | ||
+ | this.data.sensors.temperature.forEach( function( _apiTemp ) | ||
+ | { | ||
+ | var temp = this._leaflet.temperatures[ _apiTemp.name ]; | ||
+ | if ( temp ) | ||
+ | { | ||
+ | temp.setStyle({color: this._determineColor( _apiTemp.value )}); | ||
+ | |||
+ | var delta = (Date.now() - new Date( _apiTemp.ext_lastchange * 1000 )) / 1000; | ||
+ | temp.custom = { | ||
+ | description: _apiTemp.description | _apiTemp.name, | ||
+ | location: _apiTemp.location | null, | ||
+ | value: _apiTemp.value, | ||
+ | lastchange: _apiTemp.ext_lastchange | ||
+ | } | ||
+ | //updatePopup( temp | ||
+ | |||
+ | temp.bindPopup( "Description: " + _apiTemp.description + "<br/>Location: " + _apiTemp.location + "<br/>Value: " + _apiTemp.value + _apiTemp.unit + "<br/>Last update: " + this._nlsTime( delta ) + " ago" ); | ||
+ | } | ||
+ | }, this ); | ||
+ | } | ||
+ | |||
+ | var bounds = null; | ||
+ | var maxZoom = 18; | ||
+ | |||
+ | // Handle beacons | ||
+ | if ( this.data.sensors && this.data.sensors.beacon && this.data.sensors.beacon.length < 25 ) | ||
+ | { | ||
+ | var bHoaB = false; | ||
+ | var beacons = this.data.sensors.beacon.map( function( _apiBeacon ) | ||
+ | { | ||
+ | var beacon = {}; | ||
+ | |||
+ | beacon.point = L.latLng( _apiBeacon.location.lat,_apiBeacon.location.lon ); | ||
+ | beacon.marker = L.marker( beacon.point, { icon: this._leaflet.icons[ _apiBeacon.name ] || new L.Icon.Default() } ).addTo( this._leaflet.map ); | ||
+ | beacon.circle = L.circle( beacon.point, _apiBeacon.location.accuracy, {stroke:0} ).addTo( this._leaflet.map ); | ||
+ | |||
+ | var delta = (Date.now() - new Date( _apiBeacon.ext_lastchange * 1000 )) / 1000; | ||
+ | |||
+ | // Closure variable | ||
+ | if ( ( delta < 3600 ) && ( _apiBeacon.name === "HoaB" || _apiBeacon.name === "HoT" ) ) | ||
+ | bHoaB = true; | ||
+ | |||
+ | if ( this._leaflet.icons[ _apiBeacon.name ] ) | ||
+ | this._leaflet.icons[ _apiBeacon.name ].options.className = delta > 60 ? "disconnected" : ""; | ||
+ | |||
+ | var popup = beacon.marker.getPopup(); | ||
+ | if ( !popup ) | ||
+ | popup = beacon.marker.bindPopup().getPopup(); | ||
+ | |||
+ | popup.setContent( ( this._leaflet.descriptions[ _apiBeacon.name ] || _apiBeacon.name ) + "<br/>Last update: " + this._nlsTime( delta ) + " ago" ); | ||
+ | |||
+ | return beacon; | ||
+ | }, this ); | ||
+ | |||
+ | // TODO: clean up old beacons!! | ||
+ | this._leaflet.beacons.forEach( function( _beacon ) | ||
+ | { | ||
+ | // Destroy popup | ||
+ | _beacon.marker.unbindPopup( ); | ||
+ | |||
+ | // Remove marker and circle | ||
+ | this._leaflet.map.removeLayer( _beacon.marker ); | ||
+ | this._leaflet.map.removeLayer( _beacon.circle ); | ||
+ | }, this ); | ||
+ | /* | ||
+ | for ( var b = 0; b < Math.min( this._leaflet.beacons.length, beacons, length ); b++ ) | ||
+ | { | ||
+ | // Update position, icon, radius, tooltip | ||
+ | this._leaflet.beacons[ b ] | ||
+ | } | ||
+ | */ | ||
+ | this._leaflet.beacons = beacons; | ||
+ | |||
+ | // Only follow beacons automatically initially if there is a HoaB among it | ||
+ | if ( bHoaB && this._leaflet.follow === null ) | ||
+ | { | ||
+ | this._leaflet.follow = true; | ||
+ | document.querySelector( "div.leaflet-bar > a.leaflet-clickable" ).className = "leaflet-clickable toggle"; | ||
+ | } | ||
+ | } | ||
+ | else if (this.data.sensors && this.data.sensors.beacon && this.data.sensors.beacon.length) | ||
+ | { | ||
+ | var apiBeacon = this.data.sensors.beacon[ 0 ]; | ||
+ | var beacon = {}; | ||
+ | var bHoaB = false; | ||
+ | |||
+ | beacon.point = L.latLng( apiBeacon.location.lat,apiBeacon.location.lon ); | ||
+ | beacon.marker = L.marker( beacon.point, { icon: this._leaflet.icons[ apiBeacon.name ] || new L.Icon.Default() } ).addTo( this._leaflet.map ); | ||
+ | beacon.circle = L.circle( beacon.point, apiBeacon.location.accuracy, {stroke:0} ).addTo( this._leaflet.map ); | ||
+ | |||
+ | var delta = (Date.now() - new Date( apiBeacon.ext_lastchange * 1000 )) / 1000; | ||
+ | |||
+ | // Closure variable | ||
+ | if ( ( delta < 3600 ) && ( apiBeacon.name === "HoaB" || apiBeacon.name === "HoT" ) ) | ||
+ | bHoaB = true; | ||
+ | |||
+ | if ( this._leaflet.icons[ apiBeacon.name ] ) | ||
+ | this._leaflet.icons[ apiBeacon.name ].options.className = delta > 60 ? "disconnected" : ""; | ||
+ | |||
+ | var popup = beacon.marker.getPopup(); | ||
+ | if ( !popup ) | ||
+ | popup = beacon.marker.bindPopup().getPopup(); | ||
+ | |||
+ | popup.setContent( ( this._leaflet.descriptions[ apiBeacon.name ] || apiBeacon.name ) + "<br/>Last update: " + this._nlsTime( delta ) + " ago" ); | ||
+ | |||
+ | // TODO: clean up old beacons!! | ||
+ | this._leaflet.beacons.forEach( function( _beacon ) | ||
+ | { | ||
+ | // Destroy popup | ||
+ | _beacon.marker.unbindPopup( ); | ||
+ | |||
+ | // Remove marker and circle | ||
+ | this._leaflet.map.removeLayer( _beacon.marker ); | ||
+ | this._leaflet.map.removeLayer( _beacon.circle ); | ||
+ | }, this ); | ||
+ | this._leaflet.beacons = [ beacon ]; | ||
+ | |||
+ | if ( bHoaB && this._leaflet.follow === null ) | ||
+ | { | ||
+ | this._leaflet.follow = true; | ||
+ | document.querySelector( "div.leaflet-bar > a.leaflet-clickable" ).className = "leaflet-clickable toggle"; | ||
+ | } | ||
+ | |||
+ | this._drawBeaconPolyLine( ); | ||
+ | |||
+ | maxZoom = 10; | ||
+ | } | ||
+ | |||
+ | // TODO: Update if coordinate is incorrect | ||
+ | if ( this._leaflet.follow === null ) | ||
+ | { | ||
+ | this._leaflet.follow = false; | ||
+ | |||
+ | // Update location | ||
+ | this._leaflet.point = L.latLng( this.data.location.lat, this.data.location.lon ); | ||
+ | this._leaflet.marker.setLatLng( this._leaflet.point ); | ||
+ | this._leaflet.map.setZoom( 18 ); | ||
+ | |||
+ | |||
+ | // Set popup data and open it | ||
+ | var popup = this._leaflet.marker.getPopup(); | ||
+ | if ( !popup ) | ||
+ | popup = this._leaflet.marker.bindPopup().getPopup(); | ||
+ | |||
+ | var info = '<img src="' + this.data.logo + '" style="max-width:'+this._width+'px;width:100%"><br/>'; | ||
+ | var l = this.data.location, s = this.data.spacefed; | ||
+ | info += l.address+"<br/>"; | ||
+ | |||
+ | if ( l.ext_floor ) | ||
+ | info += "floor " + l.ext_floor; | ||
+ | if ( l.ext_room ) | ||
+ | info += ", room " + l.ext_room; | ||
+ | info += "<br/>☎ " + '<a target="blank" href="tel:+'+this.data.contact.phone+'">+'+this.data.contact.phone+'</a>'; | ||
+ | info += "<br/>" + this._tristate( s.spacenet ) + ' <a target="blank" href="Spacenet">spacenet</a>'; | ||
+ | info += "<br/>" + this._tristate( s.ext_spacenet5g ) + " spacenet (5GHz)"; | ||
+ | info += "<br/>" + this._tristate( s.spacesaml ) + " spacesaml"; | ||
+ | info += "<br/>" + this._tristate( s.ext_spaceconnect ) + " spaceconnect"; | ||
+ | info += "<br/>" + this._tristate( s.spacephone ) + ' <a target="blank" href="Spacephone">spacephone</a>'; | ||
+ | if ( s.ext_spacephone_extension ) | ||
+ | info += ": E" + s.ext_spacephone_extension; | ||
+ | |||
+ | popup.setContent( info ); | ||
+ | |||
+ | this._leaflet.marker.openPopup(); | ||
+ | //popup.update(); | ||
+ | } | ||
+ | |||
+ | if ( this._leaflet.follow ) | ||
+ | { | ||
+ | // Determine the bounding box to 'follow | ||
+ | bounds = L.latLngBounds( this._leaflet.beacons.map( function( _beacon ) | ||
+ | { | ||
+ | return _beacon.point; | ||
+ | } ) ); | ||
+ | |||
+ | // If we don't have Hackers on a Bike, include the home location together with the other beacons | ||
+ | if ( !bHoaB ) | ||
+ | bounds.extend( this._leaflet.point ); | ||
+ | |||
+ | // Disable zoom (in) if already zoomed beyond set max | ||
+ | if ( this._leaflet.map.getZoom() > maxZoom ) | ||
+ | { | ||
+ | this._leaflet.map.panTo( bounds.getCenter() ); | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | this._leaflet.map.fitBounds( bounds ); | ||
+ | if ( this._leaflet.map.getZoom() > maxZoom ) | ||
+ | this._leaflet.map.setZoom( maxZoom ); | ||
+ | } | ||
+ | } | ||
+ | } // leaflet | ||
+ | |||
+ | if ( this._features.split(",").indexOf( "annex" ) >= 0 ) | ||
+ | { | ||
+ | var node = this._node; | ||
+ | [].forEach.call(document.querySelectorAll(".state.annex"),function(_n) | ||
+ | { | ||
+ | node.removeChild( _n ); | ||
+ | }); | ||
+ | |||
+ | (this.data.sensors&&this.data.sensors.service||[]).filter(function(_s) | ||
+ | { | ||
+ | return (_s.source==="annex" && _s.name!=="annex"); | ||
+ | }).forEach(function(_d,_i) | ||
+ | { | ||
+ | var anode = node.appendChild( document.createElement( "div" ) ); | ||
+ | anode.style.position = "absolute"; | ||
+ | anode.style.width = "0.4em"; | ||
+ | anode.style.height = "0.4em"; | ||
+ | anode.style.top = "calc(2em + 10px)"; | ||
+ | anode.style.right = (0.5 * _i) + "em"; | ||
+ | anode.style.backgroundColor = (_d.status|0)?"green":"red"; | ||
+ | anode.style.zIndex = 500; | ||
+ | anode.className = "state annex"; | ||
+ | var t = new Date( 0 ); | ||
+ | t.setUTCSeconds( _d.ext_lastchange ); | ||
+ | anode.title = _d.name + " (updated " + t.toLocaleString( )+")"; | ||
+ | }); | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | SpaceAPI.prototype._tristate = function( _state ) | ||
+ | { | ||
+ | if ( _state ) | ||
+ | return "✅"; | ||
+ | else if ( _state === null ) | ||
+ | return "❓"; | ||
+ | else | ||
+ | return "❌"; | ||
+ | } | ||
+ | |||
+ | SpaceAPI.prototype._xhr_onerror = function( ) | ||
+ | { | ||
+ | // Something has failed, show the error | ||
+ | this._updateState( this._msgError, this._colorUnknown ); | ||
+ | } | ||
+ | } | ||
+ | var state; | ||
+ | //state = new SpaceAPI( "auto", "auto", "none", "8px-->", "//ackspace.nl/spaceAPI/", 15, "beacon" ); | ||
+ | state = new SpaceAPI( "<!--{$width|escape:html|default:auto}-->", "<!--{$height|escape:html|default:auto}-->", "<!--{$float|escape:html|default:none}-->", "<!--{$padding|escape:html|default:8px}-->", "<!--{$url}-->", <!--{$interval|validate:int|default:0}-->, "<!--{$features|escape:'quotes'}-->" ); | ||
+ | state.start(); | ||
}( )); | }( )); | ||
</script> | </script> | ||
</includeonly> | </includeonly> |
Latest revision as of 16:51, 24 April 2023
This widget allows you to display the Space API data (provided as JSON)
Created by Xopr
Using this widget
To insert this widget, use the following code:
{{#widget:SpaceAPI |url=/spaceAPI/ |width=260px |height=300px |padding=8px |interval=20 |float=right |features=beacon,annex }}
This will give the following result:
Notes
- url is mandatory, the rest is optional (leave out interval to make the data static).
- it also must be written without protocol since colon (:) is not allowed, and may be relative, for example: //ackspace.nl/spaceAPI/ or /spaceAPI/
- You must provide a unit for the sizes (i.e. px, %, etc.)
Copy to your site
To use this widget on your site, just install MediaWiki Widgets extension and copy full source code of this page to your wiki as Widget:SpaceAPI article.