diff options
Diffstat (limited to 'src/leaflet-soton.js')
-rw-r--r-- | src/leaflet-soton.js | 1851 |
1 files changed, 1851 insertions, 0 deletions
diff --git a/src/leaflet-soton.js b/src/leaflet-soton.js new file mode 100644 index 0000000..accd7f7 --- /dev/null +++ b/src/leaflet-soton.js @@ -0,0 +1,1851 @@ +(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(); + } + } + }, + getFeatureByURI: function(uri) { + var features, feature; + + var names = Object.keys(LS.data); + + for (var i=0; i<names.length; i++) { + features = LS.data[names[i]].features; + + for (var j=0; j<features.length; j++) { + feature = features[j]; + + if ("uri" in feature.properties && + feature.properties.uri === uri) { + return feature; + } + } + } + + return null; + }, + _updateWorkstationData: function() { + var query; + + if (this.data.workstations.features.length > 10) { + query = 'PREFIX soton: <http://id.southampton.ac.uk/ns/>\ +SELECT * WHERE {\ + ?uri soton:workstationSeats ?total_seats .\ + ?uri soton:workstationFreeSeats ?free_seats .\ + ?uri soton:workstationStatus ?status .\ + FILTER (\ + ?free_seats >= 0\ + )\ +}'; + } else { + query = 'PREFIX soton: <http://id.southampton.ac.uk/ns/>\ +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 © <a href="http://openstreetmap.org">OpenStreetMap</a> 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) { + + // Adding .features means leaflet will + // ignore those without a geometry + map.indoorLayer = L.indoorLayer(data.buildingParts.features, { + level: map._startLevel, + style: function(feature) { + var fill = 'white'; + if (feature.properties.buildingpart === 'corridor') { + fill = '#169EC6'; + } + + 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 && partWorkstation.feature in workstationData) { + var workstationIcon = '<div class="ls-workstationicon" style="margin-left: auto; margin-right: auto; background-image: url(' + LS.imagePath + 'workstation-group.png' + ')"><div style="padding-left: 26px;">'; + + var freeSeats = workstationData[partWorkstation.feature].free_seats; + + workstationIcon += freeSeats + "</div></div>"; + + 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) { + layer.on('click', function(e) { + var content; + var popupOptions = {}; + + // When the feature is clicked on + if ("buildingpart" in feature.properties) { + content = roomPopupTemplate(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<buildings.length; i++) { + var building = buildings[i]; + + if (building.properties.uri === uri) { + var close; + + var content = buildingTemplate(building.properties, + this.options.indoor, + map, + function() { close(); }); + + var temp = { _rings: building.geometry.coordinates, _map: this }; + + + var center = building.geometry.coordinates[0][0]; + center = [center[1], center[0]]; + + map.panTo(center); + + close = showPopup(map, content, center); + + return; + } + } + }, + showByURI: function(uri) { + var feature = LS.getFeatureByURI(uri); + + if (feature === null) { + throw "can't find " + uri; + } + + if (!("geometry" in feature)) { + throw "no location for " + uri; + } + + if (feature.geometry.type === "Polygon") { + var coords = L.GeoJSON.coordsToLatLngs(feature.geometry.coordinates[0], 0, L.GeoJSON.coordsToLatLng); + var bounds = L.latLngBounds(coords); + this.fitBounds(bounds); + if ("level" in feature.properties) { + this.setLevel(feature.properties.level); + } + this.closePopup(); + + return; + } else if (feature.geometry.type === "Point") { + this.setView(L.GeoJSON.coordsToLatLng(feature.geometry.coordinates), 22); + if ("level" in feature.properties) { + this.setLevel(feature.properties.level); + } + this.closePopup(); + return; + } else { + throw "unable to handle " + feature.geometry.type; + } + } + }); + + LS.map = function (id, options) { + return new LS.Map(id, options); + }; + + function showPopup(map, content, latlng, popupOptions) { + popupOptions = popupOptions || {}; + + popupOptions.maxWidth = map.getContainer().offsetWidth; + + var close; + + if (false && smallScreen()) { + // Just in case there is a popup open, as the screen has just shrunk + map.closePopup(); + + var containerWrapper = document.getElementById('dynamicContentWrapper'); + containerWrapper.style.display = 'block'; + + var container = document.getElementById('dynamicContent'); + + var contentDiv = document.createElement('div'); + + var closeButton = L.DomUtil.create('button', 'close', container); + closeButton.setAttribute('aria-hidden', 'true'); + closeButton.setAttribute('type', 'button'); + closeButton.textContent = 'x'; + + close = closeButton.onclick = function() { + container.innerHTML = ''; + containerWrapper.style.display = 'none'; + }; + + container.appendChild(content); + } else { + var popup = L.popup(popupOptions).setLatLng(latlng); + + popup.setContent(content); + + popup.openOn(map); + + close = function() { + map.closePopup(popup); + }; + } + + return close; + } + + // Template functions for creating the popups + + var popupTemplates = { + sites: siteTemplate, + buildings: buildingTemplate, + bicycleParking: bicycleParkingTemplate, + parking: parkingTemplate, + busStops: busStopTemplate + }; + + function roomPopupTemplate(properties) { + properties = L.extend({}, properties); + + if (!("name" in properties)) { + properties.name = "Room "; + if ("ref" in properties) { + properties.name += properties.ref; + } + } + + return getTemplateWrapper(properties, function(content) { + + var tabs = [ + { + id: 'features', + name: 'Features', + active: true + }, + { + id: 'contents', + name: 'Contents', + }, + { + id: 'bookings', + name: 'Bookings', + }, + { + id: 'pictures', + name: 'Pictures', + }]; + + tabs = createTabs(tabs, content); + + if ('contents' in properties) { + properties.contents.forEach(function(feature) { + createBlankLink(feature.feature, feature.label, tabs.features); + + if (feature.subject === "http://id.southampton.ac.uk/point-of-interest-category/iSolutions-Workstations") { + var content = '<a href="' + feature.feature + '">' + feature.label + '</a>'; + var data = properties.data; + if (typeof data !== 'undefined' && 'total_seats' in data) { + content += '<br>' + data.status; + content += '<br>' + data.free_seats + ' seats free (' + data.total_seats + ' total seats)'; + } + return content; + } else { + return '<a href="' + feature.feature + '">' + feature.label + '</a>'; + } + }); + } + + if ('images' in properties) { + properties.images.forEach(function(image) { + + }); + } + }); + } + + 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<versions.length; i++) { + var version = versions[i]; + url = version.url; + + imageWidth = version.width; + imageHeight = version.height; + + var mapContainer = map.getContainer(); + var widthBound = mapContainer.offsetWidth; + var heightBound = mapContainer.offsetHeight; + + if (!smallScreen()) { + widthBound *= 0.7; + heightBound *= 0.7; + } + + if (imageWidth < widthBound && + imageHeight < heightBound) { + break; // Use this image, as it is the first smaller + // than the screen width + } + } + + if (url !== null) { + // Link to the biggest image (versions is sorted by size on the server) + var imageLink = createBlankLink(versions[0].url, false, tabs.picture); + + tabs.picture.style.minWidth = imageWidth + "px"; + tabs.picture.style.minHeight = imageHeight + "px"; + + var image = document.createElement('img'); + image.setAttribute('src', url); + image.setAttribute('width', imageWidth); + image.setAttribute('height', imageHeight); + + imageLink.appendChild(image); + + //createBlankLink(properties.images[0].licence, "Licence", tabs.picture); + } else { + tabs.picture.textContent = "No Image Available"; + } + } else { + + } + + var energyIFrame = document.createElement('iframe'); + energyIFrame.setAttribute('src', 'http://data.southampton.ac.uk/time-series?action=fetch&series=elec/b' + properties.loc_ref + '/ekw&type=average&format=graph&resolution=3600&startTime=1375009698000.0'); + energyIFrame.setAttribute('frameBorder', '0'); + energyIFrame.setAttribute('style', 'width: 100%; height 100%;'); + + tabs.energyUsage.style.minWidth = imageWidth + "px"; + tabs.energyUsage.style.minHeight = imageHeight + "px"; + tabs.energyUsage.appendChild(energyIFrame); + + createBlankLink('http://data.southampton.ac.uk/time-series?action=fetch&series=elec/b' + properties.loc_ref + '/ekw&type=average&format=graph&resolution=3600&startTime=0', 'Graph for all time', tabs.energyUsage); + + // Rooms + if (indoor) { + + tabs.rooms.style.minWidth = imageWidth + "px"; + tabs.rooms.style.minHeight = imageHeight + "px"; + + tabs.rooms.style.maxWidth = imageWidth + "px"; + tabs.rooms.style.maxHeight = imageHeight + "px"; + + tabs.rooms.style.overflow = 'scroll'; + + for (var level in properties.rooms) { + var rooms = properties.rooms[level]; + + // Heading + + var panelTitle = L.DomUtil.create('h4', '', tabs.rooms); + panelTitle.textContent = "Level " + level; + + // Content + + var contentPanel = L.DomUtil.create('div', '', tabs.rooms); + + rooms.forEach(function(uri) { + var room = LS.getFeatureByURI(uri); + + if (room === null) { + console.err("Unable to find room " + uri); + return; + } + + var teachingAndBookable = ""; + + if (room.properties.teaching) { + teachingAndBookable += " (T) "; + } + + if (room.properties.teaching) { + teachingAndBookable += " (B)"; + } + + var roomProps = document.createTextNode(teachingAndBookable); + + var description = room.properties.ref; + if ("name" in room.properties) { + description += ": " + room.properties.name; + } + + if ("center" in room.properties) { + var roomLink = createLink('#', false, tabs.rooms); + + roomLink.onclick = function() { + close(); + map.showByURI(uri); + }; + + roomLink.textContent = description; + } else { + var roomText = document.createTextNode(description); + + tabs.rooms.appendChild(roomText); + } + + tabs.rooms.appendChild(roomProps); + + var moreInfo = createBlankLink(uri, "(More Information)", tabs.rooms); + moreInfo.style.cssFloat = moreInfo.style.styleFloat = "right"; + + tabs.rooms.appendChild(document.createElement('br')); + }); + } + + tabs.services.style.minWidth = imageWidth + "px"; + tabs.services.style.minHeight = imageHeight + "px"; + + tabs.services.style.maxWidth = imageWidth + "px"; + tabs.services.style.maxHeight = imageHeight + "px"; + + tabs.services.style.overflow = 'scroll'; + + if ("services" in properties) { + if ("vendingMachines" in properties.services) { + title = L.DomUtil.create('h4', '', tabs.services); + title.textContent = "Vending Machines"; + + properties.services.vendingMachines.forEach(function(machine) { + var feature = LS.getFeatureByURI(machine); + + if (feature === null) { + console.error("no feature for " + machine); + return; + } + + if ("geometry" in feature) { + var machineLink = createLink('#', false, tabs.services); + + machineLink.onclick = function() { + close(); + map.showByURI(machine); + }; + + machineLink.textContent = feature.properties.label; + } else { + var note = document.createTextNode(feature.properties.label); + + tabs.services.appendChild(note); + } + + var moreInfo = createBlankLink(machine, "(More Information)", tabs.services); + moreInfo.style.cssFloat = moreInfo.style.styleFloat = "right"; + + tabs.services.appendChild(document.createElement('br')); + }); + } + + if ("mfds" in properties.services) { + var title = L.DomUtil.create('h4', '', tabs.services); + title.textContent = "Multi-Function Devices"; + + properties.services.mfds.forEach(function(mfd) { + var feature = LS.getFeatureByURI(mfd); + + if (feature === null) { + console.error("no feature for " + mfd); + return; + } + + if ("geometry" in feature) { + var mfdLink = createLink('#', false, tabs.services); + + mfdLink.onclick = function() { + close(); + map.showByURI(mfd); + }; + + mfdLink.textContent = feature.properties.label; + } else { + var note = document.createTextNode(feature.properties.label); + + tabs.services.appendChild(note); + } + + var moreInfo = createBlankLink(mfd, "(More Information)", tabs.services); + moreInfo.style.cssFloat = moreInfo.style.styleFloat = "right"; + + tabs.services.appendChild(document.createElement('br')); + }); + } + } + } + }); + } + + function parkingTemplate(properties) { + if (!('name' in properties)) + properties.name = 'Car Park'; + + return getTemplateWrapper(properties, function(content) { + var table = createPropertyTable( + [ + "Access", + "Spaces", + "Fee" + ], + [ + properties.access, + properties.capacity, + properties.fee + ] + ); + + content.appendChild(table); + }); + } + + function bicycleParkingTemplate(properties) { + if (!('name' in properties)) + properties.name = 'Bicycle Parking'; + + return getTemplateWrapper(properties, function(content) { + var table = createPropertyTable( + [ + "Capacity", + "Type", + "Covered" + ], + [ + properties.capacity, + properties.bicycle_parking, + properties.covered + ] + ); + + content.appendChild(table); + }); + } + + function busStopTemplate(properties) { + return getTemplateWrapper(properties, function(content) { + + /*<tr><td>Routes: \ + <% each(properties.routes, function(route) { %>\ + <font color="<%= route.colour %>"><%= route.ref %></font>\ + <% }); %>\ + </td></tr>\ + <tr>\ + <td colspan=2><iframe width="400px" height="100%" style="border: solid 1px #000" src="<%\ + var parts = properties.uri.split("/");\ + var id = parts[parts.length - 1].split(".")[0];\ + print("http://data.southampton.ac.uk/bus-stop/" + id + ".html?view=iframe");\ + %>" frameBorder="0"></iframe></td>\ + </tr>\*/ + + /*var parts = properties.uri.split("/"); + var id = parts[parts.length - 1].split(".")[0]; + var src = "http://data.southampton.ac.uk/bus-stop/" + id + ".html?view=iframe"; + + var energyIFrame = document.createElement('iframe'); + energyIFrame.setAttribute('src', src); + energyIFrame.setAttribute('frameBorder', '0'); + energyIFrame.setAttribute('style', 'width: 100%; height 100%;'); + + content.appendChild(energyIFrame);*/ + + }); + } + + function busRouteTemplate(properties) { + return getTemplateWrapper(properties, function(content) { + + /* + * note + */ + + }); + } + + // Templating Utility Functions + + function capitaliseFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } + + function getEnergyGraph(properties) { + + return html; + } + + function getTemplateWrapper(properties, contentFunction) { + var table = document.createElement('table'); + + table.classList.add("ls-content-table"); + + table.setAttribute('style', 'width: 100%'); + + var headingRow = document.createElement('tr'); + table.appendChild(headingRow); + + var titleData = document.createElement('td'); + headingRow.appendChild(titleData); + + var title = document.createElement('h2'); + title.classList.add("ls-popup-title"); + titleData.appendChild(title); + + var titleText = ""; + + if ('loc_ref' in properties) { + titleText += properties.loc_ref + ' '; + } + + if ('name' in properties) { + titleText += properties.name; + } + + title.textContent = titleText; + + var moreInfo = L.DomUtil.create('td', '', headingRow); + moreInfo.setAttribute('align', 'right'); + + if ('uri' in properties) { + createBlankLink(properties.uri, '(More Information)', moreInfo); + } + + var contentRow = L.DomUtil.create('tr', '', table); + + var contentData = L.DomUtil.create('td', '', contentRow); + contentData.setAttribute('colspan', '2'); + + contentFunction(contentData); + + return table; + } + + var createTabs = function(tabs, container) { + + var nav = L.DomUtil.create('ul', 'ls-nav ls-nav-tabs', container); + var content = L.DomUtil.create('div', 'tab-content', container); + + var tabDivs = {}; + + var activeDiv; + var activeLi; + + tabs.forEach(function(tab) { + var li = L.DomUtil.create('li', '', nav); + + var a = L.DomUtil.create('a', '', li); + + a.setAttribute('href', '#'); + + a.textContent = tab.name; + + // Content + var div = L.DomUtil.create('div', 'tab-pane', content); + + if ('active' in tab && tab.active) { + activeDiv = div; + activeLi = li; + + li.classList.add('active'); + } else { + div.style.display = 'none'; + } + + a.onclick = function() { + activeDiv.style.display = 'none'; + activeLi.classList.remove('active'); + + div.style.display = 'block'; + li.classList.add('active'); + + activeDiv = div; + activeLi = li; + + return false; + }; + + tabDivs[tab.id] = div; + }); + + return tabDivs; + }; + + var createLink = function(url, target, container) { + var link = document.createElement('a'); + + link.setAttribute('href', url); + + if (target) + link.setAttribute('target', target); + + if (container) + container.appendChild(link); + + return link; + }; + + var createBlankLink = function(url, text, container) { + var link = createLink(url, '_blank', container); + + if (text) + link.textContent = text; + + return link; + }; + + function createPropertyTable(keys, values) { + var table = document.createElement('table'); + + keys.forEach(function(key, i) { + var tr = document.createElement('tr'); + + var keyTd = document.createElement('td'); + keyTd.textContent = key; + tr.appendChild(keyTd); + + var valueTd = document.createElement('td'); + valueTd.setAttribute('align', 'right'); + valueTd.textContent = values[i] || "Unknown"; + tr.appendChild(valueTd); + + table.appendChild(tr); + }); + + return table; + } + + // General Utility Functions + + function getBusTimes(uri) { + var parts = uri.split("/"); + var id = parts[parts.length - 1].split(".")[0]; + + return "http://data.southampton.ac.uk/bus-stop/" + id + ".html?view=iframe"; + } + + function getJSON(options, callback) { + var xhttp = new XMLHttpRequest(); + xhttp.ontimeout = function () { + callback(null); + }; + xhttp.timeout = 2000; + + options.data = options.data || null; + + xhttp.open('GET', options.url, true); + xhttp.setRequestHeader('Accept', 'application/json'); + + xhttp.send(options.data); + xhttp.onreadystatechange = function() { + if (xhttp.status == 200 && xhttp.readyState == 4) { + callback(JSON.parse(xhttp.responseText)); + } + }; + } + + function smallScreen() { + return window.innerWidth < 500; + } + + // Custom Hash Support + + if ("Hash" in L) { + LS.Hash = L.Class.extend(L.extend({}, L.Hash.prototype, { + initialize: function (map, showLevel) { + this.showLevel = showLevel; + L.Hash.call(this, map); + + var hash = this; + + map.on("levelchange", function() { + hash.onMapMove(); + }); + }, + parseHash: function(hash) { + var startOfSecondPart = hash.indexOf("/"); + var firstPart = hash.slice(0, startOfSecondPart); + + if (firstPart.indexOf('#') === 0) { + firstPart = firstPart.substr(1); + } + + var newLevel = parseInt(firstPart, 10); + if (!isNaN(newLevel) && newLevel !== this.map.getLevel()) { + this.map.setLevel(newLevel); + } + + var secondPart = hash.slice(startOfSecondPart + 1); + + return L.Hash.prototype.parseHash.call(this, secondPart); + }, + formatHash: function(map) { + var hash = L.Hash.prototype.formatHash.call(this, map); + + var levelString = map.getLevel() + ''; + + hash = "#" + levelString + '/' + hash.slice(1); + + return hash; + } + })); + } + + var WorkstationIcon = L.DivIcon.extend({ + initialize: function(workstations, workstationState) { + var html = '<div style="padding-left: 26px;">'; + + var freeSeats = 0; + + var allClosed = true; + + var openIcon = { + iconUrl: LS.imagePath + "workstation-group.png", + iconSize: [66, 32], + iconAnchor: [33, 16], + className: 'ls-workstationicon' + } + + var closedIcon = { + iconUrl: LS.imagePath + "workstation-closed.png", + iconSize: [32, 32], + iconAnchor: [16, 16], + className: 'ls-workstationicon' + } + + workstations.forEach(function(workstation) { + if (workstation in workstationState) { + var state = workstationState[workstation]; + + var closed = (state.status.indexOf("closed") !== -1) + allClosed = allClosed && closed; + + freeSeats += workstationState[workstation].free_seats; + } + }); + + this._closed = allClosed; + + var iconUrl; + + if (!this._closed) { + html += freeSeats + "</div>"; + + openIcon.html = html; + + L.setOptions(this, openIcon); + } else { + html += "</div>"; + + L.setOptions(this, closedIcon); + } + }, + 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); + }; + + if (typeof state !== 'undefined') { + var text = document.createTextNode(" " + state.free_seats + " free seats (" + state.total_seats + " total seats)"); + 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.Control.Level = L.Control.extend({ + includes: L.Mixin.Events, + + options: { + position: 'bottomright', + parseLevel: function(level) { + return parseInt(level, 10); + } + }, + + initialize: function(options) { + L.setOptions(this, options); + + this._map = null; + this._buttons = {}; + this._listeners = []; + this._level = options.level; + + this.addEventListener("levelchange", this._levelChange, this); + }, + onAdd: function(map) { + var div = L.DomUtil.create('div', 'ls-levelselector'); + + var btnGroup = L.DomUtil.create('div', 'ls-btn-group', div); + + var buttons = this._buttons; + var activeLevel = this._level; + var self = this; + + this.options.levels.forEach(function(level) { + var cls = 'ls-btn'; + + var levelNum = self.options.parseLevel(level); + + if (level === activeLevel || levelNum === activeLevel) + cls += ' active'; + + var levelBtn = L.DomUtil.create('button', cls, btnGroup); + + levelBtn.appendChild(levelBtn.ownerDocument.createTextNode(level)); + + levelBtn.onclick = function() { + self.setLevel(level); + }; + + buttons[level] = levelBtn; + }); + + return div; + }, + _levelChange: function(e) { + // Probably won't work in some browsers, see + // https://developer.mozilla.org/en-US/docs/Web/API/element.classList + + if (this._map !== null) { + this._buttons[e.oldLevel].classList.remove('active'); + this._buttons[e.newLevel].classList.add('active'); + } + }, + setLevel: function(level) { + + if (level === this._level) + return; + + var oldLevel = this._level; + this._level = level; + + this.fireEvent("levelchange", { + oldLevel: oldLevel, + newLevel: level + }); + }, + getLevel: function() { + return this._level; + } +}); + +L.Control.level = function (options) { + return new L.Control.Level(options); +}; + +/** + * A layer that will display indoor data + * + * addData takes a GeoJSON feature collection, each feature must have a level + * property that indicates the level. If the level is a string, some function + * will be used to rank the levels. + * + * getLevels can be called to get the array of levels that are present. + * + * + */ + +L.IndoorLayer = L.Class.extend({ + + initialize: function(data, options) { + L.setOptions(this, options); + + var onEachFeature = options.onEachFeature; + var layers = this._layers = {}; + this._map = null; + if ("level" in options) { + this._level = options.level; + } else { + this._level = null; + } + + this.options.onEachFeature = function(feature, layer) { + + onEachFeature(feature, layer); + + var marker = options.markerForFeature(feature); + if (typeof(marker) !== 'undefined') { + marker.on('click', function(e) { + layer.fire('click', e); + }); + + layers[feature.properties.level].addLayer(marker); + } + }; + + this.addData(data); + }, + addTo: function (map) { + map.addLayer(this); + return this; + }, + onAdd: function (map) { + this._map = map; + + if (this._level === null) { + var levels = this.getLevels(); + + if (levels.length !== 0) { + this._level = levels[0]; + } + } + + this._map.addLayer(this._layers[this._level]); + }, + onRemove: function (map) { + this._map.removeLayer(this._layers[this._level]); + this._map = null; + }, + addData: function(data) { + var layers = this._layers; + + var options = this.options; + + var features = L.Util.isArray(data) ? data : data.features; + + features.forEach(function (part) { + var level = part.properties.level; + var layer; + + if (typeof level === 'undefined') + return; + + if (!("geometry" in part)) { + return; + } + + if (level in layers) { + layer = layers[level]; + } else { + layer = layers[level] = L.geoJson({ type: "FeatureCollection", features: [] }, options); + } + + layer.addData(part); + }); + }, + getLevels: function() { + return Object.keys(this._layers); + }, + getLevel: function() { + return this._level; + }, + setLevel: function(level) { + if (typeof(level) === 'object') { + level = level.newLevel; + } + + if (this._level === level) + return; + + var oldLayer = this._layers[this._level]; + var layer = this._layers[level]; + + if (this._map !== null) { + this._map.removeLayer(oldLayer); + this._map.addLayer(layer); + } + + this._level = level; + } +}); + +L.indoorLayer = function(data, options) { + return new L.IndoorLayer(data, options); +}; + +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.IndoorLayer(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; + }; +} |