(function() { "use strict"; var LS = window.LS = L.extend({}, L.Mixin.Events, { dataPath: 'data.json', imagePath: 'images/', data: null, _dataFetchInProgress: false, workstationData: null, _workstationDataFetchInProgress: false, getData: function(callback) { if (this.data !== null) { callback(this.data); } else { this.on("dataload", callback); if (!this._dataFetchInProgress) { this._dataFetchInProgress = true; getJSON({url: LS.dataPath} , function(data) { LS.data = data; LS._dataFetchInProgress = false; LS.fire("dataload", data); }); } } }, getWorkstationData: function(callback) { if (this.workstationData !== null) { // TODO: Some kind of periodic refresh callback(this.workstationData); } else { this.addOneTimeEventListener("workstationData", callback); if (!this._workstationDataFetchInProgress) { this._workstationDataFetchInProgress = true; this._updateWorkstationData(); } } }, getRoomFor: function(uri) { var parts = LS.data.buildingParts.features; for (var i=0; i 10) { query = 'PREFIX soton: \ SELECT * WHERE {\ ?uri soton:workstationSeats ?total_seats .\ ?uri soton:workstationFreeSeats ?free_seats .\ ?uri soton:workstationStatus ?status .\ FILTER (\ ?free_seats >= 0\ )\ }'; } else { query = 'PREFIX soton: \ SELECT * WHERE {\ ?uri soton:workstationSeats ?total_seats .\ ?uri soton:workstationFreeSeats ?free_seats .\ ?uri soton:workstationStatus ?status .\ FILTER ('; query += '(' + workstations.features.map(function(workstation) { return '?uri = <' + workstation.properties.uri + '> '; }).join(' || '); query += ') && ?free_seats >= 0'; query += ')}'; } getJSON({ url: 'http://sparql.data.southampton.ac.uk/?query=' + encodeURIComponent(query) }, function(data) { LS.workstationData = {}; if (data === null) { LS.fire("workstationData", null); return; } data.results.bindings.forEach(function(result) { var workstation = result.uri.value; var id = "#" + workstation.split('/').slice(-1)[0]; var obj = {}; obj.total_seats = parseInt(result.total_seats.value, 10); obj.free_seats = parseInt(result.free_seats.value, 10); obj.status = result.status.value; LS.workstationData[workstation] = obj; }); LS._workstationDataFetchInProgress = false; LS.fire("workstationData", LS.workstationData); } ); } }); var busRouteColours = {}; var icons = { created: false, createIcons: function() { this.printer = L.icon({ iconUrl: LS.imagePath + 'printer.png', iconSize: [32, 37], // size of the icon iconAnchor: [16, 37], // point of the icon which will correspond to marker's location popupAnchor: [0, -35] // point from which the popup should open relative to the iconAnchor }); this.vendingHotDrinks = L.icon({ iconUrl: LS.imagePath + 'coffee.png', iconSize: [32, 37], iconAnchor: [16, 37], popupAnchor: [0, -35] }); this.vendingSweets = L.icon({ iconUrl: LS.imagePath + 'candy.png', iconSize: [32, 37], iconAnchor: [16, 37], popupAnchor: [0, -35] }); this.toiletsUnisex = L.icon({ iconUrl: LS.imagePath + 'toilets.png', iconSize: [32, 32], iconAnchor: [16, 16], popupAnchor: [0, -35] }); this.toiletsMale = L.icon({ iconUrl: LS.imagePath + 'toilets-m.png', iconSize: [32, 32], iconAnchor: [16, 16], popupAnchor: [0, -35] }); this.toiletsFemale = L.icon({ iconUrl: LS.imagePath + 'toilets-f.png', iconSize: [32, 32], iconAnchor: [16, 16], popupAnchor: [0, -35] }); this.toiletsDisabled = L.icon({ iconUrl: LS.imagePath + 'toilets_disability.png', iconSize: [32, 32], iconAnchor: [16, 37], popupAnchor: [0, -35] }); this.theBridge = L.icon({ iconUrl: LS.imagePath + 'logos/the-bridge.png', iconSize: [300, 80], iconAnchor: [150, 40], popupAnchor: [0, 0] }); this.theStags = L.icon({ iconUrl: LS.imagePath + 'logos/stags-head.png', iconSize: [300, 80], iconAnchor: [150, 40], popupAnchor: [0, 0] }); this.theSUSUShop = L.icon({ iconUrl: LS.imagePath + 'logos/susu-shop.png', iconSize: [300, 80], iconAnchor: [150, 40], popupAnchor: [0, 0] }); this.theSUSUCafe = L.icon({ iconUrl: LS.imagePath + 'logos/susu-cafe.png', iconSize: [300, 80], iconAnchor: [150, 40], popupAnchor: [0, 0] }); this.created = true; } }; var blankStyle = function(feature) { return { weight: 0, opacity: 0, fillOpacity: 0 }; }; var showStyle = function(feature) { return { weight: 1, opacity: 1, fillOpacity: 1 }; }; var emptyFeatureCollection = { type: "FeatureCollection", features: [] }; var transparaentStyle = function(feature) {return {weight: 0, opacity: 0, fillOpacity: 0};}; var layerNames = ['sites', 'parking', 'bicycleParking', 'buildings', 'busStops' /*'busRoutes',*/]; var busRouteStyle = function(feature) { return {weight: 5, opacity: 0.5, color: feature.properties.colour}; }; LS.Map = L.Map.extend({ options: { center: [50.9354, -1.3964], indoor: false, workstations: false, zoom: 17, tileUrl: 'http://bus.southampton.ac.uk/graphics/map/tiles/{z}/{x}/{y}.png', tileAttribution: 'Map data © OpenStreetMap contributors' }, initialize: function (id, options) { options = L.setOptions(this, options); L.Map.prototype.initialize.call(this, id, options); var map = this; if (!("layers" in options) || options.layers.length === 0) { var tileLayer = L.tileLayer(options.tileUrl, { maxZoom: 22, attribution: options.tileAttribution }); tileLayer.addTo(map); } if (!("MarkerClusterGroup" in L)) { options.workstations = false; } if (!("highlight" in options)) { options.highlight = {}; } var overlayMaps = { //"Bus Routes": self.layers.busRoutes, }; if ("Hash" in L) { var hash; if (this.options.indoor) { hash = new LS.Hash(this); } else { hash = new L.Hash(this); } } if (!icons.created) { icons.createIcons(); } var layers = {}; var showingIndoorControl = false; var showLevel = null; map._startLevel = options.level || "1"; layerNames.forEach(function(layerName) { var layerOptions = { style: function(feature) { if (feature.properties.uri in options.highlight && options.highlight[feature.properties.uri]) { return {weight: 5, opacity: 0.5, color: 'blue'}; } else if (layerName === "busRoutes") { return busRouteStyle(); } else { return blankStyle(); } } }; if (layerName === 'buildings') { layerOptions.onEachFeature = function(feature, layer) { // When the feature is clicked on layer.on('click', function(e) { var close; var content = buildingTemplate(feature.properties, options.indoor, map, function() { close(); }); close = showPopup(map, content, e.latlng); }); }; } else { layerOptions.onEachFeature = function(feature, layer) { // When the feature is clicked on layer.on('click', function(e) { var content = popupTemplates[layerName](feature.properties); showPopup(map, content, e.latlng); }); }; } if (layerName === "bicycleParking") { layerOptions.pointToLayer = function (feature, latlng) { return L.circleMarker(latlng, { radius: 8, opacity: 1, }); }; } if (layerName === "busStops") { layerOptions.pointToLayer = function (feature, latlng) { return L.circleMarker(latlng, { radius: 8, opacity: 1, }); }; } layers[layerName] = L.geoJson(emptyFeatureCollection, layerOptions).addTo(map); }); this.on('zoomend', function(e) { var zoom = this.getZoom(); // The buildingParts layer wants to show on zooms > 19, that is 20, 21 and 22 // The sites layer wants to show on zoom levels less than 18, that is 17 - 1 if (zoom <= 15) { if (!(this.hasLayer(layers.sites))) { this.addLayer(layers.sites, true); } } else if (zoom > 15) { if (this.hasLayer(layers.sites)) { this.removeLayer(layers.sites); } } }); LS.getData(function(data) { for (var layerName in layers) { var layer = layers[layerName]; layer.clearLayers(); layer.addData(data[layerName]); } LS.getWorkstationData(function(workstationData) { if (options.indoor) { // Adding .features means leaflet will // ignore those without a geometry map.indoorLayer = L.indoor(data.buildingParts.features, { level: map._startLevel, style: function(feature) { var fill = 'white'; if (feature.properties.buildingpart === 'corridor') { fill = '#169EC6'; } else if (feature.properties.buildingpart === 'verticalpassage') { fill = '#0A485B'; } return { fillColor: fill, weight: 1, color: '#666', fillOpacity: 1 }; }, markerForFeature: function(part) { if (part.properties.buildingpart === "room") { var iconCoords = part.properties.center; if (part.properties.name === "The Bridge") { return L.marker(iconCoords, {icon: icons.theBridge}); } else if (part.properties.name === "SUSU Shop") { return L.marker(iconCoords, {icon: icons.theSUSUShop}); } else if (part.properties.name === "The Stag's") { return L.marker(iconCoords, {icon: icons.theStags}); } else if (part.properties.name === "SUSU Cafe") { return L.marker(iconCoords, {icon: icons.theSUSUCafe}); } var partWorkstation = null; if ('contents' in part.properties) { part.properties.contents.forEach(function(feature) { if (feature.subject === "http://id.southampton.ac.uk/point-of-interest-category/iSolutions-Workstations") { partWorkstation = feature; } }); } if (part.properties.amenity === "toilets") { if ("male" in part.properties) { return L.marker(iconCoords, {icon: icons.toiletsMale}); } else if ("female" in part.properties) { return L.marker(iconCoords, {icon: icons.toiletsFemale}); } else if ("unisex" in part.properties) { return L.marker(iconCoords, {icon: icons.toiletsUnisex}); } // TODO: Disabled } var content; if ("name" in part.properties && "ref" in part.properties) { content = part.properties.name + " (" + part.properties.ref + ")"; } else if ("ref" in part.properties) { content = "Room " + part.properties.ref; } else if ("name" in part.properties) { content = part.properties.name; } else { return; } if (partWorkstation) { if (partWorkstation.feature in workstationData) { var state = workstationData[partWorkstation.feature]; var closed = (state.status.indexOf("closed") !== -1) var image; var workstationIcon; if (!closed) { image = 'workstation-group.png'; workstationIcon = '
'; } else { image = 'workstation-closed.png'; workstationIcon = '
'; } if (!closed) { workstationIcon += '
'; var freeSeats = state.free_seats; workstationIcon += freeSeats + "
"; } workstationIcon += '
'; content = workstationIcon + content; } else { var workstationIcon = '
'; content = workstationIcon + content; } } var myIcon = L.divIcon({ className: 'ls-room-marker', html: content, iconSize: new L.Point(100, 30), iconAnchor: new L.Point(50, 15) }); var marker = L.marker(iconCoords, {icon: myIcon}); return marker; } }, onEachFeature: function(feature, layer) { if (feature.properties.buildingpart === "corridor") { return; // No popup for corridors yet } layer.on('click', function(e) { var content; var popupOptions = {}; // When the feature is clicked on if ("buildingpart" in feature.properties) { if (feature.properties.buildingpart === "room") { content = roomPopupTemplate(feature.properties); } else if (feature.properties.buildingpart === "verticalpassage") { content = verticalPassagePopupTemplate(feature.properties); } } else { // Assume that it is a printer // TODO: Use different icons where appropriate popupOptions.offset = icons.vendingHotDrinks.options.popupAnchor; if ('vending' in feature.properties) { content = vendingPopupTemplate(feature.properties); } else { content = printerPopupTemplate(feature.properties); } } showPopup(map, content, e.latlng, popupOptions); }); }, pointToLayer: function (feature, latlng) { var icon; if ('vending' in feature.properties) { if (feature.properties.vending === 'drinks') { icon = icons.vendingHotDrinks; } else if (feature.properties.vending === 'sweets') { icon = icons.vendingSweets; } else { console.warn("Unrecognired vending " + feature.properties.vending); } } else { icon = icons.printer; } return L.marker(latlng, {icon: icon}); } }); map.indoorLayer.addData(data.buildingFeatures); map.levelControl = L.Control.level({ levels: map.indoorLayer.getLevels(), level: map._startLevel }); map.levelControl.addEventListener("levelchange", map.indoorLayer.setLevel, map.indoorLayer); map.levelControl.on("levelchange", function(e) { map.fireEvent("levelchange", e); }); } var workstationMarkerLayer; if (options.workstations) { workstationMarkerLayer = LS.workstationLayer(); } if (options.indoor) { var setIndoorContent = function(zoom) { if (zoom <= 19) { if (showingIndoorControl) { map.levelControl.removeFrom(map); showingIndoorControl = false; } if (map.hasLayer(map.indoorLayer)) { map.removeLayer(map.indoorLayer); } if (options.workstations && !map.hasLayer(workstationMarkerLayer)) { map.addLayer(workstationMarkerLayer); } } else if (zoom > 19) { if (!showingIndoorControl) { map.levelControl.addTo(map); showingIndoorControl = true; } if (!map.hasLayer(map.indoorLayer)) { map.addLayer(map.indoorLayer); } if (options.workstations && map.hasLayer(workstationMarkerLayer)) { map.removeLayer(workstationMarkerLayer); } } }; setIndoorContent(map.getZoom()); map.on('zoomend', function(e) { setIndoorContent(this.getZoom()); }); } else { if (options.workstations) { map.addLayer(workstationMarkerLayer); } } }); }); return this; }, setLevel: function(level) { if ("levelControl" in this) { this.levelControl.setLevel(level); } else { this._startLevel = level; } }, getLevel: function() { if ("levelControl" in this) { return this.levelControl.getLevel(); } else { return this._startLevel; } }, show: function(thing) { this.showByURI(thing); }, showPopupByURI: function(uri) { var map = this; var buildings = LS.data.buildings.features; for (var i=0; i' + feature.label + ''; var data = properties.data; if (typeof data !== 'undefined' && 'total_seats' in data) { content += '
' + data.status; content += '
' + data.free_seats + ' seats free (' + data.total_seats + ' total seats)'; } return content; } else { return '' + feature.label + ''; } }); } if ('images' in properties) { properties.images.forEach(function(image) { }); } }); } function verticalPassagePopupTemplate(properties) { properties = L.extend({}, properties); if (!("name" in properties)) { if (properties["buildingpart:verticalpassage"] === "stairway") { properties.name = "Stairway"; } else { properties.name = "Vertical Passage"; } if ("ref" in properties) { properties.name += properties.ref; } } return getTemplateWrapper(properties, function(content) { if ("level" in properties) { content.appendChild(document.createTextNode("Levels:")); var levelList = document.createElement("ul"); properties.level.forEach(function(level) { var levelLi = document.createElement("li"); levelLi.textContent = level; levelList.appendChild(levelLi); }); content.appendChild(levelList); } }); } function printerPopupTemplate(properties) { properties.name = "Printer"; return getTemplateWrapper(properties, function(content) { }); } function vendingPopupTemplate(properties) { properties.name = "Vending Machine"; return getTemplateWrapper(properties, function(content) { content.textContent = properties.vending; }); } function siteTemplate(properties) { return getTemplateWrapper(properties, function(content) { }); } function buildingTemplate(properties, indoor, map, close) { return getTemplateWrapper(properties, function(content) { var buildingTabs = [ { id: 'picture', name: 'Pictures', active: true }, { id: 'energyUsage', name: 'Energy Usage', }]; if (indoor) { buildingTabs.push({ id: 'rooms', name: 'Facilities', }); buildingTabs.push({ id: 'services', name: 'Services', }); } var tabs = createTabs(buildingTabs, content); var imageWidth; var imageHeight; if (properties.images.length !== 0) { var versions = properties.images[0].versions; var url; for (var i=0; i"; openIconWithState.html = html; L.setOptions(this, openIconWithState); } } else { L.setOptions(this, generalIcon); } }, createIcon: function (oldIcon) { var div = L.DivIcon.prototype.createIcon.call(this, oldIcon); div.style.backgroundImage = "url(" + this.options.iconUrl + ")"; return div; } }); if ("MarkerClusterGroup" in L) { LS.WorkstationLayer = L.MarkerClusterGroup.extend({ initialize: function() { var workstations = {}; LS.data.workstations.features.forEach(function(feature) { workstations[feature.properties.uri] = feature.properties; }); var workstationLayer = this; var workstationsTemplate = function(workstationURIs) { var div = document.createElement('div'); var headerText = "Workstation"; if (workstationURIs.length !== 1) { headerText = "Workstations"; } var header = document.createElement('h2'); header.textContent = headerText; div.appendChild(header); workstationURIs.forEach(function(uri) { var workstation = workstations[uri]; var state = workstationData[uri]; var link = createLink("#", null, div); link.textContent = workstation.label; link.onclick = function() { workstationLayer._map.showByURI(uri); }; var text; if (typeof state !== 'undefined') { var closed = (state.status.indexOf("closed") !== -1) if (!closed) { text = document.createTextNode(" " + state.free_seats + " free seats (" + state.total_seats + " total seats) " + state.status); } else { text = document.createTextNode(" " + state.status); } } else { text = document.createTextNode(" State Unknown"); } div.appendChild(text); var br = document.createElement("br"); div.appendChild(br); }); return div; }; var workstationData = {}; L.MarkerClusterGroup.prototype.initialize.call(this, { spiderfyOnMaxZoom: false, showCoverageOnHover: false, zoomToBoundsOnClick: false, iconCreateFunction: function(cluster) { var uris = cluster.getAllChildMarkers().map(function(marker) { return marker.uri; }); return new WorkstationIcon(uris, workstationData); } }); LS.getWorkstationData(function(data) { workstationData = data; LS.data.workstations.features.forEach(function(workstation) { var icon = new WorkstationIcon([workstation.properties.uri], workstationData); var marker = new L.Marker(L.GeoJSON.coordsToLatLng(workstation.geometry.coordinates), { icon: icon }); marker.uri = workstation.properties.uri; workstationLayer.addLayer(marker); }); workstationLayer.on('click', function (a) { var uri = a.layer.uri; var popupOptions = {offset: [0, -15]}; var content = workstationsTemplate([uri]); showPopup(this._map, content, a.latlng, popupOptions); }).on('clusterclick', function (a) { var uris = a.layer.getAllChildMarkers().map(function(marker) { return marker.uri; }); var popupOptions = {offset: [0, -15]}; var content = workstationsTemplate(uris); showPopup(this._map, content, a.latlng, popupOptions); }); }); return this; } }); LS.workstationLayer = function () { return new LS.WorkstationLayer(); }; } })(); L.SelectiveVisibilityLayer = L.Class.extend({ _layers: {}, initialize: function(options) { L.setOptions(this, options); }, onAdd: function (map) { this._map = map; if (this.options.level === null) { var levels = this.getLevels(); if (levels.length !== 0) { this.options.level = levels[0]; } } this._map.addLayer(this._layers[this.options.level]); }, onRemove: function (map) { this._map = null; }, addLayer: function(level, options) { var layers = this._layers; options = this.options; data.features.forEach(function (part) { var level = part.properties.level; var layer; if (typeof level === 'undefined') return; if (level in layers) { layer = layers[level]; } else { layer = layers[level] = L.geoJson({ type: "FeatureCollection", features: [] }, options); } layer.addData(part); }); } }); L.SelectiveVisibilityLayer = function(data, options) { return new L.Indoor(data, options); }; // forEach compatability if (!Array.prototype.forEach) { Array.prototype.forEach = function (fn, scope) { 'use strict'; var i, len; for (i = 0, len = this.length; i < len; ++i) { if (i in this) { fn.call(scope, this[i], i, this); } } }; } // map function compatability if (!Array.prototype.map) { Array.prototype.map = function(callback, thisArg) { var T, A, k; if (this === null) { throw new TypeError(" this is null or not defined"); } // 1. Let O be the result of calling ToObject passing the |this| value as the argument. var O = Object(this); // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". // 3. Let len be ToUint32(lenValue). var len = O.length >>> 0; // 4. If IsCallable(callback) is false, throw a TypeError exception. // See: http://es5.github.com/#x9.11 if (typeof callback !== "function") { throw new TypeError(callback + " is not a function"); } // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. if (thisArg) { T = thisArg; } // 6. Let A be a new array created as if by the expression new Array(len) where Array is // the standard built-in constructor with that name and len is the value of len. A = new Array(len); // 7. Let k be 0 k = 0; // 8. Repeat, while k < len while(k < len) { var kValue, mappedValue; // a. Let Pk be ToString(k). // This is implicit for LHS operands of the in operator // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. // This step can be combined with c // c. If kPresent is true, then if (k in O) { // i. Let kValue be the result of calling the Get internal method of O with argument Pk. kValue = O[ k ]; // ii. Let mappedValue be the result of calling the Call internal method of callback // with T as the this value and argument list containing kValue, k, and O. mappedValue = callback.call(T, kValue, k, O); // iii. Call the DefineOwnProperty internal method of A with arguments // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true}, // and false. // In browsers that support Object.defineProperty, use the following: // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true }); // For best browser support, use the following: A[ k ] = mappedValue; } // d. Increase k by 1. k++; } // 9. return A return A; }; }