Difference between revisions of "Widget:SpaceAPI"

From Hackerspace ACKspace
Jump to: navigation, search
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=20px
+
|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=20px
+
|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";
+
"use strict";
  
    if ( typeof SpaceAPI === "undefined" )
+
/*
    {
+
TODO:
        window.SpaceAPI = function( _width, _height, _float, _padding, _url, _interval, _features )
+
separate rooms (get latlng)
        {
+
zoom onto toolbox
            this._width = _width;
+
table island not correct (4, not 5 tables)
            this._height = _height;
+
            this._padding = _padding;
+
*/
            this._url = _url;
 
            this._interval = 1000 * _interval;
 
            this._float = _float;
 
            this._features = _features;
 
        }
 
  
        SpaceAPI.prototype.data            = null;
+
if ( typeof SpaceAPI === "undefined" )
        SpaceAPI.prototype._width          = null;
+
{
        SpaceAPI.prototype._height        = null;
+
window.SpaceAPI = function( _width, _height, _float, _padding, _url, _interval, _features )
        SpaceAPI.prototype._float          = null;
+
{
        SpaceAPI.prototype._url            = null;
+
this._width = _width;
        SpaceAPI.prototype._features      = null;
+
this._height = _height;
        SpaceAPI.prototype._interval       = null;
+
this._padding = _padding;
        SpaceAPI.prototype._intervalId    = null;
+
this._url = _url;
        SpaceAPI.prototype._node          = null;
+
this._interval = 1000 * _interval;
        SpaceAPI.prototype._beacon        = null;
+
this._float = _float;
        SpaceAPI.prototype._msgLoading    = "Loading..";
+
this._features = _features;
        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.start = function( )
+
SpaceAPI.prototype.data = null;
        {
+
SpaceAPI.prototype._width   = null;
            // Use interval timer id as image id
+
SpaceAPI.prototype._height = null;
            this._intervalId = 0;
+
SpaceAPI.prototype._float   = null;
            if ( this._interval > 0 )
+
SpaceAPI.prototype._url = null;
                this._intervalId = setInterval( this._fetchState.bind( this ), this._interval );
+
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;
  
            var style = "width:" + this._width + ";text-align:center;-moz-box-shadow: 3px 3px 4px #000;-webkit-box-shadow: 3px 3px 4px rgba(0,0,0,0.2);box-shadow: 3px 3px 4px rgba(0,0,0,0.2);position:relative;";
+
SpaceAPI.prototype._drawBeaconPolyLine = function( )
            if ( this._float )
+
{
                style += "float:" + this._float + ";"
+
if ( !this.data.sensors || !this.data.sensors.beacon || this.data.sensors.beacon.length < 50 )
            document.write( '<div id="spaceAPI' + this._intervalId + '"></div>' );
+
return;
  
            var node = document.getElementById( "spaceAPI" + this._intervalId );
+
var points = [];
            node.style = style;
+
var bounds = this._leaflet.map.getBounds();
            this._node = node.appendChild( document.createElement( "div" ) );
 
            this._node.style.height = this._height + "px";
 
            this._node.style.padding = this._padding;
 
            this._node.textContent = this._msgLoading;
 
  
            if ( this._features.split(",").indexOf( "beacon" ) >= 0 )
+
// Only add points within the bounds
            {
+
if ( this._leaflet.beaconCount !== this.data.sensors.beacon.length )
                var srcNode;
+
{
                var mapNode = node.appendChild( document.createElement( "div" ) );
+
this._leaflet.beaconCount = this.data.sensors.beacon.length;
                mapNode.style.width = "100%";
+
for ( var n = 0, len = this.data.sensors.beacon.length; n < len; ++n )
                mapNode.style.height = "276px";
+
{
               
+
var point = L.latLng( this.data.sensors.beacon[n].location.lat, this.data.sensors.beacon[n].location.lon );
                srcNode = document.createElement( "link" );
+
points.push( point );
                srcNode.href = "/leaflet/leaflet.css";
+
}
                srcNode.rel = "stylesheet";
 
                srcNode.type = "text/css";
 
  
                // Load the css
+
if ( !this._leaflet.beaconline )
                ( document.head || document.documentElement ).appendChild( srcNode );
+
this._leaflet.beaconline = L.polyline(points, {color: 'green'}).addTo( this._leaflet.map );
               
+
else
                srcNode = document.createElement( "script" );
+
this._leaflet.beaconline.setLatLngs( points );
                srcNode.src = "/leaflet/leaflet.js";
+
}
                srcNode.type = "text/javascript";
+
};
                srcNode.addEventListener( "load", function( _evt )
 
                {
 
                    this._beacon = {};
 
                    this._beacon.point = L.latLng( 50.883333, 5.983333 );
 
                    this._beacon.map = L.map( mapNode ).setView( this._beacon.point, 11);
 
  
                    L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+
SpaceAPI.prototype.start = function( )
                        attribution: '&copy; <a href="//openstreetmap.org/copyright">OpenStreetMap</a> contributors'
+
{
                    }).addTo( this._beacon.map );
+
// Use interval timer id as image id
 +
this._debug = ( location.hash.split("#").slice(1).indexOf("debug") !== -1 );
  
                    // "Follow" control
+
this._intervalId = 0;
                    this._beacon.follow = true;
+
if ( this._interval > 0 )
                    L.Control.Command = L.Control.extend(
+
this._intervalId = setInterval( this._fetchState.bind( this ), this._interval );
                    {
 
                        options:
 
                        {
 
                            position: 'topleft',
 
                        },
 
               
 
                        onAdd: function( _map )
 
                        {
 
                            var controlDiv = L.DomUtil.create( "div", "leaflet-bar" );
 
                            var controlUI = L.DomUtil.create( "a", "leaflet-clickable" + (this._beacon.follow ? " toggle" : ""), controlDiv );
 
                            controlUI.innerHTML = "&#8982;";
 
                            controlUI.style.fontSize = "35px";
 
                            controlUI.title = 'Follow';
 
               
 
                            L.DomEvent.addListener( controlUI, 'click', function( _evt )
 
                            {
 
                                this._beacon.follow = !this._beacon.follow;
 
                                _evt.currentTarget.className = "leaflet-clickable" + (this._beacon.follow ? " toggle" : "");
 
               
 
                                console.log( _evt.currentTarget, this )
 
                            }.bind( this ) );
 
               
 
                            return controlDiv;
 
                        }.bind( this )
 
                    } );
 
  
                    // Icons
+
document.write( '<div id="spaceAPI' + this._intervalId + '"></div>' );
                    this._beacon.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]
 
                        } )
 
                    };
 
  
                    this._beacon.marker = L.marker( this._beacon.point, { /*icon: myIcon*/ } ).addTo( this._beacon.map );
+
var node = document.getElementById( "spaceAPI" + this._intervalId );
                    this._beacon.circle = L.circle( this._beacon.point, 80, { stroke: false } ).addTo( this._beacon.map );
+
if ( !node )
                }.bind( this ) );
+
{
 +
console && console.log( "node not found" );
 +
return;
 +
}
 +
node.style.width = this._width;
 +
node.style.height = this._height;
  
                // Load the script
+
node.style.textAlign = "center";
                ( document.head || document.documentElement ).appendChild( srcNode );
+
node.style.BoxShadow = "3px 3px 4px rgba(0,0,0,0.2)";
            }
+
node.style.position = "relative";
  
            // Update the space state immediately
+
if ( this._float )
            setTimeout( this._fetchState.bind( this ), 1 );
+
node.style.float = this._float;
        };
 
  
        SpaceAPI.prototype._fetchState = function( )
+
this._node = node.appendChild( document.createElement( "a" ) );
        {
+
this._node.style.display = "block";
            this._node.className = "processing";
+
this._node.style.color = "inherit";
            var xhr = new XMLHttpRequest( );
+
this._node.style.padding = this._padding;
            if ( !!( "onload" in xhr ) )
+
this._node.textContent = this._msgLoading;
            {
+
this._node.href = "/spaceAPI/statechanges.html";
                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 );
+
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";
  
            // Tells server that this call is made for ajax purposes.
+
// Load the css
            // Most libraries like jQuery/Prototype/Dojo do this
+
( document.head || document.documentElement ).appendChild( srcNode );
            xhr.setRequestHeader( "X-Requested-With", "XMLHttpRequest" );
+
 +
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);
  
            // No data needs to be sent along with the request.
+
L.CRS.CustomZoom = L.extend({}, L.CRS.EPSG3857,
            xhr.send( null );
+
{
        };
+
scale: function( zoom ) {
 +
if ( zoom < 24 )
 +
return 256 * Math.pow( 2, zoom );
  
        SpaceAPI.prototype._updateState = function( _message, _color, _title )
+
// Freeze the actual zooming above this level to show the different floors
        {
+
// 256 * pow( 2, 23 )
            this._node.className = "";
+
return 2147483648;
            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 )
+
/**********************************/
        {
+
L.TileLayer.Functional = L.TileLayer.extend({
            var open = null;
 
            var message = null;
 
            var title = null;
 
  
            try
+
  _tileFunction: null,
            {
 
                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)
+
  initialize: function (tileFunction, options) {
                var d = new Date( 0 );
+
this._tileFunction = tileFunction;
                d.setUTCSeconds( this.data.state.lastchange );
+
L.TileLayer.prototype.initialize.call(this, null, options);
                title = this._msgSince + d.toLocaleString( );
+
  },
            }
 
            catch( _e )
 
            {
 
                message = this._msgParserError;
 
            }
 
  
            if ( open )
+
  getTileUrl: function (tilePoint) {
                this._updateState( message || this._msgOpen, this._colorOpen, title );
+
var map = this._map,
            else if ( open === false )
+
  crs = map.options.crs,
                this._updateState( message || this._msgClosed, this._colorClosed, title );
+
  tileSize = this.options.tileSize,
            else
+
  zoom = tilePoint.z,
                this._updateState( message || this._msgUnknown, this._colorUnknown, title );
+
  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(',');
  
            if ( this._beacon )
+
// Setup object to send to tile function.
            {
+
var view = {
                if ( this.data.sensors && this.data.sensors.beacon && this.data.sensors.beacon.length )
+
  bbox: bbox,
                {
+
  width: tileSize,
                    // Draw the beacons (for now, only draw the first one)
+
  height: tileSize,
                    this._beacon.point = L.latLng( this.data.sensors.beacon[0].location.lat, this.data.sensors.beacon[0].location.lon );
+
  zoom: zoom,
                    this._beacon.circle.setRadius( this.data.sensors.beacon[0].location.accuracy );
+
  tile: {
 +
row: this.options.tms ? this._tileNumBounds.max.y - tilePoint.y : tilePoint.y,
 +
column: tilePoint.x
 +
  },
 +
  subdomain: this._getSubdomain(tilePoint)
 +
};
  
                    var delta = (Date.now() - new Date( this.data.sensors.beacon[0].ext_lastchange * 1000 )) / 1000;
+
return this._tileFunction(view);
                    var postfix = "s";
+
  },
                    if ( this._beacon.icons[ this.data.sensors.beacon[0].name ] )
 
                        this._beacon.icons[ this.data.sensors.beacon[0].name ].options.className = delta > 60 ? "disconnected" : "";
 
                    if ( delta > 31556952 )
 
                    {
 
                        delta /= 31556952;
 
                        postfix = "Y";
 
                    }
 
                    else if ( delta > 2629746 )
 
                    {
 
                        delta /= 2629746;
 
                        postfix = "M";
 
                    }
 
                    else if ( delta > 604800 )
 
                    {
 
                        delta /= 604800;
 
                        postfix = "W";
 
                    }
 
                    else if ( delta > 86400 )
 
                    {
 
                        delta /= 86400;
 
                        postfix = "d";
 
                    }
 
                    else if ( delta > 3600 )
 
                    {
 
                        delta /= 3600;
 
                        postfix = "h";
 
                    }
 
                    else if ( delta > 60 )
 
                    {
 
                        delta /= 60;
 
                        postfix = "m";
 
                    }
 
                    else
 
                    {
 
                        postfix = "s";
 
                    }
 
                    var popup = this._beacon.marker.getPopup();
 
                    if ( !popup )
 
                        popup = this._beacon.marker.bindPopup( "." ).getPopup();
 
  
                    popup.setContent( "Hackers on a Bike<br/>Last update: " + Math.round( delta ) + postfix + " ago" );
+
  _loadTile: function (tile, tilePoint) {
 +
tile._layer = this;
 +
tile.onload = this._tileOnLoad;
 +
tile.onerror = this._tileOnError;
  
                    this._beacon.marker.setIcon( this._beacon.icons[ this.data.sensors.beacon[0].name ] || new L.Icon.Default() );
+
this._adjustTilePoint(tilePoint);
                }
+
var tileUrl = this.getTileUrl(tilePoint);
                else
 
                {
 
                    // Assume mandatory location of the space with a 20m radius
 
                    this._beacon.point = L.latLng( this.data.location.lat, this.data.location.lon );
 
                    this._beacon.circle.setRadius( 20 );
 
                    this._beacon.marker.setIcon( new L.Icon.Default() );
 
                    this._beacon.marker.unbindPopup( );
 
                }
 
  
                this._beacon.marker.setLatLng( this._beacon.point );
+
if (typeof tileUrl === 'string') {
                this._beacon.circle.setLatLng( this._beacon.point );
+
  tile.src = tileUrl;
                if ( this._beacon.follow )
+
  this.fire('tileloadstart', {
                    this._beacon.map.setView( this._beacon.point, 16, {} );
+
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
 +
});
 +
  });
 +
}
 +
  }
 +
});
  
        SpaceAPI.prototype._xhr_onerror = function( )
+
L.tileLayer.functional = function (tileFunction, options) {
        {
+
  return new L.TileLayer.Functional(tileFunction, options);
            // Something has failed, show the error
+
};
            this._updateState( this._msgError, this._colorUnknown );
+
/*******************************/
        }
 
    }
 
  
    var state = new SpaceAPI( "<!--{$width|escape:html|default:auto}-->", "<!--{$height|escape:html|default:auto}-->", "<!--{$float|escape:html|default:none}-->", "<!--{$padding|escape:html|default:8px}-->", "<!--{$url|escape:urlpathinfo}-->", <!--{$interval|validate:int|default:0}-->, "<!--{$features|escape:'quotes'}-->" );
+
L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    state.start();
+
attribution: '&copy; <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 = "&#8982;";
 +
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 "&#x2705;";
 +
else if ( _state === null )
 +
return "&#x2753;";
 +
else
 +
return "&#x274C;";
 +
}
 +
 
 +
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.