(function() { "use strict"; var LS = window.LS = L.extend({}, L.Mixin.Events, { dataPath: 'data.json', imagePath: 'images/', data: null, _dataFetchInProgress: false, workstationData: null, updateWorkstationData: true, // regularly poll for workstation data workstationDataUpdateTime: 30000, // time to wait between requests useLocalStorage: true, localStorageTimeout: 172800, // two days in milliseconds _workstationDataFetchInProgress: false, _workstationDataFetchTimeout: null, getData: function(callback) { if (this.data !== null) { callback(this.data); } else { this.on("dataload", callback); if (LS.useLocalStorage && 'localStorage' in window && window['localStorage'] !== null) { if ("data" in localStorage) { var refetchTime = parseInt(localStorage.dataTimestamp, 10) + LS.localStorageTimeout; if (refetchTime > new Date().getTime()) { LS.data = JSON.parse(localStorage.data); LS.fire("dataload", LS.data); return; } // refresh the data, as its too old } // data not in local storage, so fetch it } if (!this._dataFetchInProgress) { this._dataFetchInProgress = true; getJSON({url: LS.dataPath, cache: false} , function(data, stringData) { LS._dataFetchInProgress = false; if (data === null) { setTimeout(function() { LS.getData(callback); }, 5000); return; } LS.data = data; if (LS.useLocalStorage && 'localStorage' in window && window['localStorage'] !== null) { localStorage.data = stringData; localStorage.dataTimestamp = new Date().getTime(); } LS.fire("dataload", data); }); } } }, getWorkstationData: function(callback) { if (this.workstationData !== null) { callback(this.workstationData); } else { this.addOneTimeEventListener("workstationData", callback); if (this._workstationDataFetchTimeout !== null || // if a fetch is going to happen this._workstationDataFetchInProgress) { // or a fetch is in progress // data will be fetched, so return return; } 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._workstationDataFetchInProgress = false; // If fetching data has failed if (data === null) { // Only report this if there is no existing data if (LS.workstationData === null) { LS.fire("workstationData", null); } return; } LS.workstationData = {}; 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.fire("workstationData", LS.workstationData); } ); }, infoTemplates: { room: function(properties, options, map) { 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 image_dom = imageTemplate(properties, options, map, close); if( image_dom ) { content.appendChild( image_dom ); } return; if ('contents' in properties) { properties.contents.forEach(function(feature) { createBlankLink(feature.feature, feature.label, tabs.contents); if (feature.subject === "http://id.southampton.ac.uk/point-of-interest-category/iSolutions-Workstations") { var content = '' + 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 ('features' in properties) { var featureList = document.createElement("ul"); properties.features.forEach(function(feature) { var featureLi = document.createElement("li"); createBlankLink(feature.feature, feature.label, featureLi); featureList.appendChild(featureLi); }); tabs.features.appendChild(featureList); } // TODO: Find a better way to match the rooms, also add the rooms // on level 5. var bookableLibraryRoomNames = ["1A", "1B", "1C", "2A", "2B", "2C", "2D", "3A", "3B", "3C", "3D", "3E", "3F"]; if ('name' in properties && bookableLibraryRoomNames.indexOf(properties.name) != -1) { createBlankLink("http://libcal.soton.ac.uk/booking/hartleyrooms", "This room can be booked through the Library Room Booking system.", tabs.bookings); } }); }, verticalPassage: function(properties) { properties = L.extend({}, properties); if (!("name" in properties)) { if (properties["buildingpart:verticalpassage"] === "stairway") { properties.name = "Stairway"; } else if (properties["buildingpart:verticalpassage"] === "lift") { properties.name = "Lift"; } 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); } }); }, printer: function(properties) { properties.name = "Printer"; return getTemplateWrapper(properties, function(content) { }); }, vendingMachine: function(properties) { properties.name = "Vending Machine"; return getTemplateWrapper(properties, function(content) { content.textContent = properties.vending; }); }, site: function(properties) { return getTemplateWrapper(properties, function(content) { }); }, building: function(properties, options, map, close) { var indoor = options.indoor; return getTemplateWrapper(properties, function(content) { var image_dom = imageTemplate(properties, options, map, close); if (image_dom) { content.appendChild(image_dom); } var floors = {}; // Rooms if (indoor) { addRoomsToFloors(properties.rooms, floors); if ("services" in properties) { var services = properties.services; if ("vendingMachines" in services) { addVendingMachinesToFloors(services.vendingMachines, floors); } if ("mfds" in services) { addMFDsToFloors(services.mfds, floors); } } } var floor_ids = Object.keys(floors); floor_ids.sort(function(a, b) { if (a === "Unknown") return 1; if (b === "Unknown") return -1; a = parseInt(a, 10); b = parseInt(b, 10); if (a < b) { return -1; } return 1; }); floor_ids.forEach(function(floor_id) { var h4 = document.createElement( "h4" ); content.appendChild( h4 ); h4.textContent = "Floor "+floor_id; content.appendChild(renderThingSet(floors[floor_id], map, options, close) ); }); }); }, pointOfService: function(properties) { return getTemplateWrapper(properties, function(content) { var description = document.createElement("div"); if ("description" in properties) { description.innerHTML = properties.description; } else { description.textContent = "No Description Available"; } content.appendChild(description); if ("offerings" in properties) { Object.keys(properties.offerings).forEach(function(sectionURI) { var section = properties.offerings[sectionURI]; var header = document.createElement("h4"); header.textContent = section.label; content.appendChild(header); section.items.forEach(function(item) { var a = document.createElement("a"); a.textContent = item.label; a.href = item.uri; content.appendChild(a); content.appendChild(document.createElement("br")); }); }); } }); }, parking: function(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); }); }, bicycleParking: function(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); }); }, busStop: function(properties, routeLayer) { var closeListener; // getTemplateWrapper will call the function immediatly var content = getTemplateWrapper(properties, function(content) { var routesInfo = {}; var routeMasters = routeLayer.getRouteMasters(); for (var routeMasterName in routeMasters) { var routeMaster = routeMasters[routeMasterName]; for (var i in routeMaster.routes) { var route = routeMaster.routes[i]; if (properties.routes.indexOf(route.properties.ref) !== -1) { routesInfo[route.properties.ref] = route; } } } /*var routeListControl = L.Control.Route.createRouteList({ routes: routesList }, function(routeName) { routeLayer.resetRoutes(); routeLayer.highlightRoute(routeName); }); content.appendChild(routeListControl);*/ var busStop = properties.uri.slice(37); var timesTable = document.createElement("table"); timesTable.className = "ls-bus-times-table"; content.appendChild(timesTable); var loading = document.createElement("span"); loading.innerHTML = "↻"; loading.className = "ls-spinning-arrow"; timesTable.appendChild(loading); function groupStops(stops) { var result = {}; for (var i in stops) { var stop = stops[i]; if (stop.name in result) { result[stop.name].push(stop); } else { result[stop.name] = [stop]; } } return result; } function groupedStopsToList(groupedStops) { var values = []; for (var name in groupedStops) { values.push({ name: name, stops: groupedStops[name] }); } return values.sort(function(a, b) { return b.stops.length - a.stops.length; }); } function renderTimeData(stops) { var groupedStops = groupStops(stops); for (var i in properties.routes) { var rName = properties.routes[i]; if (!(rName in groupedStops)) { groupedStops[rName] = []; } } var stopsList = groupedStopsToList(groupedStops); timesTable.innerHTML = ""; for (var i in stopsList) { var routeStops = stopsList[i].stops; var name = stopsList[i].name; var tr = document.createElement("tr"); var route = document.createElement("td"); if (!(name in routesInfo)) { console.warn("skipping " + name); continue; } var a = L.Control.Route.createRouteLink(routesInfo[name], (function(name) { return function() { var wasHighlighted = routeLayer.isRouteHighlighted(name); routeLayer.resetRoutes(); if (!wasHighlighted) { routeLayer.highlightRoute(name); } }; })(routesInfo[name].properties.name)); route.appendChild(a); tr.appendChild(route); if (routeStops.length === 0) { var none = document.createElement("td"); none.textContent = "No busses"; none.colSpan = 2; if (i !== 0) { none.className = "ls-bus-times-table-seperating-td"; } tr.appendChild(none); timesTable.appendChild(tr); } else { route.rowSpan = routeStops.length; for (var j in routeStops) { var stop = routeStops[j]; var time = document.createElement("td"); var dest = document.createElement("td"); if (i != 0 && j == 0) { time.className = "ls-bus-times-table-seperating-td"; dest.className = "ls-bus-times-table-seperating-td"; } time.textContent = stop.time; dest.textContent = stop.dest; tr.appendChild(time); tr.appendChild(dest); timesTable.appendChild(tr); tr = document.createElement("tr"); } } } } UoSLive.subscribeToBusStop(busStop, function(data) { renderTimeData(data.stops); }); closeListener = function() { UoSLive.unsubscribeToBusStop(busStop); }; }); return { content: content, closeListener: closeListener }; }, busRoute: function(properties) { return getTemplateWrapper(properties, function(content) { /* * note */ }); } } }); if (LS.updateWorkstationData) { LS.on("workstationData", function(data) { LS._workstationDataFetchTimeout = setTimeout(function() { LS._workstationDataFetchTimeout = null; LS._updateWorkstationData(); }, LS.workstationDataUpdateTime); }); } var busRouteColours = {}; var icons = { created: false, createIcons: function() { this.busStop = L.icon({ iconUrl: LS.imagePath + 'busstop.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.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 }; }; function featureHasPopup( feature ) { if (feature.properties.buildingpart === "corridor") { return; // No popup for corridors yet } if (feature.properties.buildingspart === "room" && !("uri" in feature.properties)) { return false; } return true; } var emptyFeatureCollection = { type: "FeatureCollection", features: [] }; var transparaentStyle = function(feature) {return {weight: 0, opacity: 0, fillOpacity: 0};}; var layerNames = [ 'sites', // 'parking', 'bicycleParking', 'buildings']; 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, busRoutes: false, busRouteControl: false, workstations: false, zoom: 17, detectRetina: true, tileUrl: 'http://bus.southampton.ac.uk/graphics/map/tiles/{z}/{x}/{y}.png', tileAttribution: 'Map data © OpenStreetMap contributors', levelControlPosition: 'bottomright' }, initialize: function (id, options) { options = L.setOptions(this, options); if (options.busRoutes) { if (!L.Route) { console.warn("The busRoutes option requires the leaflet-route library"); } } 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 = {}; } 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"; var popupTemplateNames = { sites: "site", buildings: "building", bicycleParking: "bicycleParking", parking: "parking" } 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 { return blankStyle(); } } }; if (layerName === 'buildings') { layerOptions.onEachFeature = function(feature, layer) { // When the feature is clicked on layer.on('click', function(e) { var content = LS.infoTemplates.building(feature.properties, options, map, function() { map.closeInfo(); }); map.showInfo(content, e.latlng); }); }; } else { layerOptions.onEachFeature = function(feature, layer) { var popupName = popupTemplateNames[layerName]; // When the feature is clicked on layer.on('click', function(e) { var content = LS.infoTemplates[popupName](feature.properties); map.showInfo(content, e.latlng); }); }; } if (layerName === "bicycleParking") { layerOptions.pointToLayer = function (feature, latlng) { return L.circleMarker(latlng, { radius: 8, opacity: 1, }); }; } layers[layerName] = L.geoJson(emptyFeatureCollection, layerOptions); }); 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) { // if there is a route layer, deal with it first, as it wants // to be added before the other layers, such that it appears // underneath them (and thus has less priority in user // interactions) if ("Route" in L) { var routeLayer = map.routeLayer = new L.Route(options.busRoutes ? data.busRoutes : emptyFeatureCollection, data.busStops, { routeOptions: { onEachFeature: function(feature, layer) { layer.on('click', function(e) { var content = LS.infoTemplates.busRoute(feature.properties, routeLayer); map.showInfo(content, e.latlng); }); }, style: busRouteStyle }, stopOptions: { onEachFeature: function(feature, layer) { layer.on('click', function(e) { var template = LS.infoTemplates.busStop(feature.properties, routeLayer); map.showInfo(template.content, e.latlng, { closeListener: template.closeListener }); }); } } }); routeLayer.addTo(map); if (options.busRoutes && options.busRouteControl) { var routeControl = new L.Control.RouteSidebar(routeLayer, "sidebar", { routeMasterSort: function(a, b) { var refs = { "U1": 1, "U2": 2, "U6": 6, "U9": 9, "U1N": 10 }; return refs[a] - refs[b]; }, position: "left" }); routeControl.addTo(map); routeControl.show(); var routeSidebarControl = new L.Control.ShowRouteSidebar(routeControl); routeSidebarControl.addTo(map); } } for (var layerName in layers) { var layer = layers[layerName]; layer.clearLayers(); layer.addData(data[layerName]); layer.addTo(map); } 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) { if (feature.geometry.type === "Point") { // Assume that this is a door return { stroke: false, fillColor: "#000000", fillOpacity: 1 }; } var fill = '#def5fc'; 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 = 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, 14), iconAnchor: new L.Point(50, 7) }); var marker = L.marker(iconCoords, { icon: myIcon }); return marker; } }, onEachFeature: function(feature, layer) { if (!featureHasPopup(feature)) { return; } layer.on('click', function(e) { map.showFeaturePopup(feature, e.latlng, options); }); }, pointToLayer: function (feature, latlng) { if ('vending' in feature.properties) { return vendingPointToLayer(feature, latlng); } else if ('uri' in feature.properties && feature.properties.uri.indexOf("http://id.southampton.ac.uk/mfd/") === 0) { return L.marker(latlng, {icon: icons.printer}); } else { return L.circleMarker(latlng, { radius: 4, clickable: false }); } } }); map.indoorLayer.addData(data.vendingMachines); map.indoorLayer.addData(data.multiFunctionDevices); map.levelControl = L.Control.level({ levels: map.indoorLayer.getLevels(), level: map._startLevel, position: options.levelControlPosition }); 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(); LS.on("workstationData", function(data) { if (map.hasLayer(workstationMarkerLayer)) { map.removeLayer(workstationMarkerLayer); workstationMarkerLayer = LS.workstationLayer(); map.addLayer(workstationMarkerLayer); } else { workstationMarkerLayer = LS.workstationLayer(); } }); } if (options.indoor) { var level = 19; if (L.Browser.retina) { level = 18; } var setIndoorContent = function(zoom) { if (zoom <= level) { 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 > level) { 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); } } }; map.on('zoomend', function(e) { setIndoorContent(this.getZoom()); }); setIndoorContent(map.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 options.maxWidth) { options.minWidth = options.maxWidth; } options.maxHeight = map.getContainer().offsetHeight*0.6; map.closeInfo(); var popup = L.popup(options).setLatLng(latlng); map._popup = popup; popup.setContent(content); if (options.closeListener) { map.once('popupclose', options.closeListener); } popup.openOn(map); }, closeInfo: function() { var map = this; if (map._popup) { map.closePopup(map._popup); } }, showFeaturePopup: function(feature, latlng, options) { var map = this; var content; var popupOptions = {}; // When the feature is clicked on if ("buildingpart" in feature.properties) { if (feature.properties.buildingpart === "room") { content = LS.infoTemplates.room(feature.properties, options, map); } else if (feature.properties.buildingpart === "verticalpassage") { content = LS.infoTemplates.verticalPassage(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 = LS.infoTemplates.vendingMachine(feature.properties); } else { content = LS.infoTemplates.printer(feature.properties); } } map.showInfo(content, latlng, popupOptions); } }); LS.map = function (id, options) { return new LS.Map(id, options); }; function vendingPointToLayer(feature, latlng) { var icon; 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); } return L.marker(latlng, {icon: icon}); } // Templating Utility Functions function imageTemplate(properties, options, map, close) { if (!("images" in properties)) return false; if (properties.images.length === 0) return false; var imageWidth; var imageHeight; var versions = properties.images[0].versions; var url; var widthBound; var heightBound; if ("popupWidth" in options && "popupHeight" in options) { widthBound = options.popupWidth; heightBound = options.popupHeight; } else { var mapContainer = map.getContainer(); widthBound = mapContainer.offsetWidth; heightBound = mapContainer.offsetHeight; widthBound *= 0.7; heightBound *= 0.7; } for (var i=0; i b) { return 1; } return -1; }); levelRooms.forEach(function(uri) { var room = LS.getFeatureByURI(uri); if (room === null) { console.error("Unable to find room " + uri); return; } var info = { label: room.properties.ref, uri: uri, geo: ("center" in room.properties) }; if ("name" in room.properties) { info.label += ": " + room.properties.name; } if(!(level in floors)) { floors[level] = {}; } floors[level][uri] = info; }); } } function addVendingMachinesToFloors(vendingMachines, floors) { vendingMachines.forEach(function(machine) { var feature = LS.getFeatureByURI(machine); if (feature === null) { console.error("no feature for " + machine); return; } var info = { label: feature.properties.label, uri: feature.properties.uri, geo: ("geometry" in feature) }; var level = "Unknown"; if ("level" in feature.properties) { level = feature.properties.level; } if (!(level in floors)) { floors[level] = {}; } floors[level][feature.properties.uri] = info; }); } function addMFDsToFloors(mfds, floors) { mfds.forEach(function(machine) { var feature = LS.getFeatureByURI(machine); if (feature === null) { console.error("no feature for " + machine); return; } var info = { label: feature.properties.label, uri: feature.properties.uri, geo: ("geometry" in feature) }; var level = "Unknown"; if ("level" in feature.properties) { level = feature.properties.level; } if(!(level in floors)) { floors[level] = {}; } floors[level][feature.properties.uri] = info; }); } function renderThingSet(set, map, options, close) { var content_ids = Object.keys( set ); var content = document.createElement( 'div' ); content_ids.forEach(function(thing_id) { var info = set[thing_id]; var div = document.createElement('div'); if (info.geo) { var link = createLink('#', false, div); link.textContent = info.label link.onclick = function() { var feature = LS.getFeatureByURI(info.uri); close(); map.panByURI(info.uri, 20, { animate: true }); if (featureHasPopup(feature)) { var latlng; if (feature.geometry.type === "Polygon") { latlng = feature.properties.center; } else if (feature.geometry.type === "Point") { latlng = L.GeoJSON.coordsToLatLng(feature.geometry.coordinates); } map.showFeaturePopup(feature, latlng, options); } }; } else { var span = document.createElement("span"); span.textContent = info.label + " "; div.appendChild(span); var link = document.createElement('a'); link.setAttribute('href', info.uri); link.setAttribute('target', '_blank'); link.className = 'ls-popup-uri'; link.textContent = "(Full Information)"; div.appendChild(link); } content.appendChild(div); }); return content; } function capitaliseFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); } function getEnergyGraph(properties) { return html; } function getTemplateWrapper(properties, contentFunction) { var documentFragment = document.createDocumentFragment(); var title = document.createElement('h2'); title.classList.add("ls-popup-title"); documentFragment.appendChild(title); var titleText = ""; if ('loc_ref' in properties) { var span = document.createElement( "span" ); span.classList.add("ls-popup-title-ref"); title.appendChild( span ); span.textContent = properties.loc_ref; title.appendChild( document.createTextNode( " " ) ); } if ('name' in properties) { if ('uri' in properties) { var link = document.createElement('a'); link.setAttribute('href', properties.uri); link.className = 'ls-popup-title-name'; link.textContent = properties.name; title.appendChild(link); } else { var span = document.createElement( "span" ); span.classList.add("ls-popup-title-name"); span.textContent = properties.name; title.appendChild( span ); } } var div_inner = document.createElement("div"); div_inner.classList.add("ls-popup-content"); contentFunction(div_inner); documentFragment.appendChild( div_inner ); return documentFragment; } 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'); div.style.display = 'block'; } 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(); options.data = options.data || null; var url = options.url if ("cache" in options && options.cache == false) { url += "?" + new Date().getTime(); } xhttp.open('GET', url, true); xhttp.ontimeout = function () { callback(null); }; xhttp.timeout = 20000; xhttp.setRequestHeader('Accept', 'application/json'); xhttp.send(options.data); xhttp.onreadystatechange = function() { if (xhttp.readyState === 4) { // loaded if (xhttp.status === 200) { callback(JSON.parse(xhttp.responseText), xhttp.responseText); } else { callback(null); } } }; } 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, workstationData) { var html = '
'; var freeSeats = 0; var allClosed = true; var someStateKnown = false; var generalIcon = { iconUrl: LS.imagePath + "workstation.png", iconSize: [32, 32], iconAnchor: [16, 16], className: 'ls-workstationicon' } var openIconWithState = { 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 workstationData) { var state = workstationData[workstation]; var closed = (state.status.indexOf("closed") !== -1) allClosed = allClosed && closed; freeSeats += workstationData[workstation].free_seats; someStateKnown = true; } }); var iconUrl; if (someStateKnown) { if (allClosed) { L.setOptions(this, closedIcon); } else { html += freeSeats + "
"; 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]); this._map.showInfo(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); this._map.showInfo(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; }; }