From e4d1c918444c15e2e1480ecb332e61ec8feb7629 Mon Sep 17 00:00:00 2001 From: Christopher Baines Date: Wed, 19 Feb 2014 22:32:59 +0000 Subject: Initial commit --- .gitignore | 6 + .gitmodules | 18 + .jshintrc | 3 + config.json.default | 7 + create-data.js | 1419 +++++++++++++++++++++++ examples/basic.html | 34 + examples/buildingsearch.html | 177 +++ examples/full.html | 45 + examples/index.html | 372 +++++++ examples/indoor.html | 37 + examples/search.html | 222 ++++ examples/workstations.html | 39 + examples/zepler.html | 44 + resources/images/candy.png | Bin 0 -> 1497 bytes resources/images/coffee.png | Bin 0 -> 1131 bytes resources/images/loader.gif | Bin 0 -> 1849 bytes resources/images/locate.png | Bin 0 -> 829 bytes resources/images/locate.svg | 383 +++++++ resources/images/locate_alt.png | Bin 0 -> 1127 bytes resources/images/locate_alt.svg | 493 ++++++++ resources/images/locate_touch.png | Bin 0 -> 1095 bytes resources/images/locate_touch_alt.png | Bin 0 -> 1438 bytes resources/images/logos/stags-head.png | Bin 0 -> 10371 bytes resources/images/logos/stags-head2.png | Bin 0 -> 3813 bytes resources/images/logos/susu-cafe.png | Bin 0 -> 10145 bytes resources/images/logos/susu-cafe2.png | Bin 0 -> 3733 bytes resources/images/logos/susu-shop.png | Bin 0 -> 8662 bytes resources/images/logos/susu-shop2.png | Bin 0 -> 3181 bytes resources/images/logos/susu.png | Bin 0 -> 92944 bytes resources/images/logos/susu2.png | Bin 0 -> 7387 bytes resources/images/logos/the-bridge.png | Bin 0 -> 4921 bytes resources/images/logos/the-bridge2.png | Bin 0 -> 2467 bytes resources/images/printer.png | Bin 0 -> 740 bytes resources/images/search-icon-mobile.png | Bin 0 -> 3959 bytes resources/images/search-icon.png | Bin 0 -> 3834 bytes resources/images/spinner.gif | Bin 0 -> 1397 bytes resources/images/toilets-f.png | Bin 0 -> 915 bytes resources/images/toilets-m.png | Bin 0 -> 789 bytes resources/images/toilets.png | Bin 0 -> 752 bytes resources/images/toilets_disability.png | Bin 0 -> 1131 bytes resources/images/workstation-big.png | Bin 0 -> 633 bytes resources/images/workstation-closed.png | Bin 0 -> 601 bytes resources/images/workstation-group.png | Bin 0 -> 514 bytes resources/images/workstation.png | Bin 0 -> 557 bytes resources/leaflet | 1 + resources/leaflet-hash | 1 + resources/leaflet-locatecontrol | 1 + resources/leaflet-markercluster | 1 + resources/mfd-location | 1 + resources/syntaxhighlighter | 1 + src/leaflet-soton.css | 275 +++++ src/leaflet-soton.js | 1851 +++++++++++++++++++++++++++++++ 52 files changed, 5431 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .jshintrc create mode 100644 config.json.default create mode 100755 create-data.js create mode 100644 examples/basic.html create mode 100644 examples/buildingsearch.html create mode 100644 examples/full.html create mode 100644 examples/index.html create mode 100644 examples/indoor.html create mode 100644 examples/search.html create mode 100644 examples/workstations.html create mode 100644 examples/zepler.html create mode 100644 resources/images/candy.png create mode 100644 resources/images/coffee.png create mode 100644 resources/images/loader.gif create mode 100644 resources/images/locate.png create mode 100644 resources/images/locate.svg create mode 100644 resources/images/locate_alt.png create mode 100644 resources/images/locate_alt.svg create mode 100644 resources/images/locate_touch.png create mode 100644 resources/images/locate_touch_alt.png create mode 100755 resources/images/logos/stags-head.png create mode 100755 resources/images/logos/stags-head2.png create mode 100755 resources/images/logos/susu-cafe.png create mode 100755 resources/images/logos/susu-cafe2.png create mode 100755 resources/images/logos/susu-shop.png create mode 100755 resources/images/logos/susu-shop2.png create mode 100755 resources/images/logos/susu.png create mode 100755 resources/images/logos/susu2.png create mode 100755 resources/images/logos/the-bridge.png create mode 100755 resources/images/logos/the-bridge2.png create mode 100644 resources/images/printer.png create mode 100644 resources/images/search-icon-mobile.png create mode 100644 resources/images/search-icon.png create mode 100644 resources/images/spinner.gif create mode 100644 resources/images/toilets-f.png create mode 100644 resources/images/toilets-m.png create mode 100644 resources/images/toilets.png create mode 100644 resources/images/toilets_disability.png create mode 100644 resources/images/workstation-big.png create mode 100644 resources/images/workstation-closed.png create mode 100644 resources/images/workstation-group.png create mode 100644 resources/images/workstation.png create mode 160000 resources/leaflet create mode 160000 resources/leaflet-hash create mode 160000 resources/leaflet-locatecontrol create mode 160000 resources/leaflet-markercluster create mode 160000 resources/mfd-location create mode 160000 resources/syntaxhighlighter create mode 100644 src/leaflet-soton.css create mode 100644 src/leaflet-soton.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca72c4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +data.json +data-source.json +config.json +*.swp +*~ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d6ad98a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,18 @@ +[submodule "resources/leaflet"] + path = resources/leaflet + url = https://github.com/Leaflet/Leaflet.git +[submodule "resources/syntaxhighlighter"] + path = resources/syntaxhighlighter + url = https://github.com/alexgorbatchev/syntaxhighlighter.git +[submodule "resources/leaflet-markercluster"] + path = resources/leaflet-markercluster + url = https://github.com/Leaflet/Leaflet.markercluster.git +[submodule "resources/mfd-location"] + path = resources/mfd-location + url = git@sourcekettle.ecs.soton.ac.uk:projects/mfd-location.git +[submodule "resources/leaflet-locatecontrol"] + path = resources/leaflet-locatecontrol + url = https://github.com/domoritz/leaflet-locatecontrol.git +[submodule "resources/leaflet-hash"] + path = resources/leaflet-hash + url = https://github.com/mlevans/leaflet-hash.git diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..d4d6e48 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,3 @@ +{ + "multistr": true +} diff --git a/config.json.default b/config.json.default new file mode 100644 index 0000000..8b0ee91 --- /dev/null +++ b/config.json.default @@ -0,0 +1,7 @@ +{ + "user": "", + "password": "password", + "server": "localhost", + "port": "5432", + "database": "hampshire" +} diff --git a/create-data.js b/create-data.js new file mode 100755 index 0000000..e236a99 --- /dev/null +++ b/create-data.js @@ -0,0 +1,1419 @@ +#!/usr/bin/env node + +/* + * This does two things, combines University Open Data, and the data from the + * osm2pgsql tables in the database to add more information back in to the + * database for the tile renderer. It also produces the static json files used + * by the web clients + */ + +var S = require('string'); +S.extendPrototype(); + +var fs = require('fs'); +var http = require("http"); +var async = require("async"); +var yaml = require('js-yaml'); + +var config = require("./config.json"); + +// Get document, or throw exception on error +try { + var printers = yaml.safeLoad(fs.readFileSync('./resources/mfd-location/data.yaml', 'utf8')); +} catch (e) { + console.error(e); + return; +} + +var validationByURI = {}; + +// prefix for the database tables +var tablePrefix = "uni_"; + +var pgql = require('pg'); +var pg = null; +pgql.connect('tcp://' + config.user + ':' + + config.password + '@' + + config.server + ':' + + config.port + '/' + + config.database, function(err, client) { + if (err) { + console.error(err); + return; + } + + pg = client; + + async.series([ + loadBusData, // TODO: At the moment this puts bus data in the database, that then gets pulled back out... + function(callback) { + createTables(function(err) { + createCollections(function(err, collections) { + + async.parallel([ + function(callback) { + var workstations = {}; + + var buildings = {}; + + collections.buildings.features.forEach(function(building) { + if ("uri" in building.properties) { + buildings[building.properties.uri] = building; + } + }); + + createRooms(buildings, workstations, function(err, buildingParts) { + + collections.buildingParts = buildingParts; + + async.parallel([ + function(callback) { + getBuildingFeatures(buildings, function(err, buildingFeatures) { + collections.buildingFeatures = buildingFeatures; + callback(err); + }); + }, + function(callback) { + getUniWorkstations(workstations, function(err, workstations) { + collections.workstations = workstations; + callback(err); + }); + } + ], callback); + }); + }, + function(callback) { + getBuildingImages(collections.buildings, callback); + } + ], function(err) { + if (err) console.error(err); + + callback(err, collections); + }); + }); + }); + } + ], function(err, results) { + if (err) { + console.error(err); + process.exit(1); + } + + var collections = results[1]; + + console.log("ending database connection"); + pgql.end(); + writeDataFiles(collections, function() { + + Object.keys(validationByURI).sort().forEach(function(uri) { + if ("location" in validationByURI[uri].errors) { + //console.log(uri + " " + validationByURI[uri].errors.location); + console.log(uri + " location unknown"); + } + }); + + console.log("complete"); + + process.exit(0); + }); + }); +}); + +function createCollections(callback) { + var collectionQueries = { + buildings: 'select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as \ + polygon,name,loc_ref,uri,leisure,height \ + from uni_building where uri is not null', + parking: 'select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as polygon,\ + name,access,capacity,"capacity:disabled",fee from uni_parking', + bicycleParking: 'select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as polygon,capacity,bicycle_parking,covered from uni_bicycle_parking', + sites: 'select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as polygon,name,loc_ref,uri from uni_site', + busStops: 'select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as polygon,name,uri,routes from uni_bus_stop', + busRoutes: 'select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as polygon,name,note,colour,ref from uni_bus_route' + }; + + var names = Object.keys(collectionQueries); + + async.map(names, function(name, callback) { + createCollection(name, collectionQueries[name], callback); + }, function(err, newCollections) { + var collectionsObject = {}; + + for (var i in names) { + name = names[i]; + + collectionsObject[name] = { + type: "FeatureCollection", + features: newCollections[i] + }; + } + + callback(err, collectionsObject); + }); +} + +function createCollection(name, query, callback) { + var collection = []; + + pg.query(query, function(err, results) { + if (err) { + console.error("Query: " + query); + console.error(err); + callback(err); + return; + } + + async.map(results.rows, function(row, callback) { + var feature = {type: "Feature"}; + feature.geometry = JSON.parse(row.polygon); + delete row.polygon; + + feature.properties = row; + + for (var key in feature.properties) { + if (feature.properties[key] === null) { + delete feature.properties[key]; + } + } + + if ("center" in feature.properties) { + var center = feature.properties.center; + center = center.slice(6, -1); + center = center.split(" ").reverse(); + feature.properties.center = center; + } + + /*if (name === "buildings") { + buildings[feature.properties.uri] = feature; + }*/ + + callback(err, feature); + }, callback); + }); +} + +function createTables(callback) { + var tableSelects = { + site: "select way,name,loc_ref,uri,amenity,landuse \ + from planet_osm_polygon \ + where operator='University of Southampton'", + building: 'select way,coalesce("addr:housename", name, \'\') as name,coalesce(height::int, "building:levels"::int * 10, 10) as height,loc_ref,leisure,uri, case when coalesce("addr:housename", name, \'\')=\'\' or "addr:housename"="addr:housenumber" then true else false end as minor from planet_osm_polygon where ST_Contains((select ST_Union(way) from uni_site), way) and building is not null order by z_order,way_area desc', + parking: 'select way,name,access,capacity,"capacity:disabled",fee from planet_osm_polygon where amenity=\'parking\' and ST_Contains((select ST_Union(way) from uni_site), way)', + bicycle_parking: "select way,capacity,bicycle_parking,covered from planet_osm_polygon where amenity='bicycle_parking' and ST_Contains((select ST_Union(way) from uni_site), way) union select way,capacity,bicycle_parking,covered from planet_osm_point where amenity='bicycle_parking' and ST_Contains((select ST_Union(way) from uni_site), way)" + }; + + // Create all the tables, these contain Universtiy relevant data that is + // both further queried, and used by sum-carto + async.eachSeries(Object.keys(tableSelects), function(table, callback) { + createTable(table, tableSelects[table], callback); + }, callback); +} + +function createTable(name, query, callback) { + var tableName = tablePrefix + name; + + console.log("creating table " + tableName); + + pg.query("drop table if exists " + tableName, function(err, results) { + var fullQuery = "create table " + tableName + " as " + query; + pg.query(fullQuery, function(err, results) { + if (err) { + console.error("error creating table " + tableName); + console.error("query: " + fullQuery); + } else { + console.log("finished creating table " + tableName); + } + callback(err); + }); + }); +} + +// buildings + +function getBuildingFeatures(buildings, callback) { + async.parallel([ + function(callback) { + getPrinters(buildings, callback); + }, + function(callback) { + getVendingMachines(buildings, callback); + } + ], function(err, results) { + var features = [] + features = features.concat.apply(features, results); + + var buildingFeatures = { type: "FeatureCollection", features: features }; + + callback(err, buildingFeatures); + }); +} + +function getBuildingImages(buildings, callback) { + console.log("getting building images"); + async.each(buildings.features, function(building, callback) { + getImagesFor(building.properties.uri, function(err, images) { + building.properties.images = images; + callback(err); + }); + }, callback); +} + +// buildingParts + +function createRooms(buildings, workstations, callback) { + console.log("creating buildingParts collection"); + + async.parallel([getBuildingParts, getBuildingRelations], + function(err, results) { + var buildingParts = results[0]; + var buildingRelations = results[1]; + + var buildingPartsByURI = {}; + + async.parallel([ + function(callback) { + async.each(buildingParts, function(part, callback) { + if (part.properties.buildingpart === "room") { + + if ("ref" in part.properties && !("uri" in part.properties)) { + console.warn("room missing URI " + JSON.stringify(part.properties)); + } + + if ("uri" in part.properties) { + buildingPartsByURI[part.properties.uri] = part; + } + + async.parallel([ + function(callback) { + findRoomFeatures(part, callback); + }, + function(callback) { + findRoomContents(part, workstations, callback); + }, + function(callback) { + findRoomImages(part, callback); + }], callback); + } else { + callback(); + } + }, callback); + }, + function(callback) { + var levelRelations = [] + + // Process level relations + async.each(buildingRelations, function(buildingRelation, callback) { + getLevelRelations(buildingRelation, function(err, newLevelRelations) { + levelRelations.push.apply(levelRelations, newLevelRelations); + callback(); + }); + }, function(err) { + + osmIDToLevel = {}; + + async.each(levelRelations, function(level, callback) { + getBuildingPartMemberRefs(level, function(err, refs) { + for (var i=0; i"; + query += ')}'; + + sparqlQuery(query, function(err, data) { + if (err) { + console.error("Query " + query); + console.error(err); + } + + room.properties.features = []; + async.each(data.results.bindings, function(feature, callback) { + room.properties.features.push({feature: feature.feature.value, label: feature.label.value}); + callback(); + }, callback); + }); +} + +// workstations + +function getUniWorkstations(workstations, callback) { + var query = 'PREFIX soton: \ +PREFIX rdfs: \ +PREFIX dct: \ +PREFIX spacerel: \ +PREFIX rdf: \ +SELECT * WHERE {\ +?workstation a ;\ + dct:subject ;\ + rdfs:label ?label ;\ + spacerel:within ?building .\ + ?building rdf:type soton:UoSBuilding .\ +}'; + + sparqlQuery(query, function(err, data) { + if (err) { + console.error("Query " + query); + console.error(err); + } + + var results = data.results.bindings; + + async.each(results, function(workstation, callback) { + var uri = workstation.workstation.value, + label = workstation.label.value, + building = workstation.building.value; + + if (!(uri in workstations)) { + + getBuildingCenter(building, function(err, center) { + if (err) { + console.error("workstation err " + err); + callback(); + return; + } + + workstations[uri] = { + type: "Feature", + geometry: center, + properties: { + label: label, + uri: uri + } + }; + + callback(); + }); + } else { + callback(); + } + }, function(err) { + var features = Object.keys(workstations).map(function(workstation) { + return workstations[workstation]; + }); + + var workstationsFeatureCollection = { type: "FeatureCollection", features: features }; + + callback(null, workstationsFeatureCollection); + }); + }); +} + +// buildingFeatures + +function getPrinters(buildings, callback) { + console.log("begining create printers"); + + var query = "PREFIX spacerel: \ +PREFIX soton: \ +PREFIX rdfs: \ +PREFIX ns1: \ +PREFIX rdf: \ +SELECT * WHERE {\ + ?mdf a ;\ + rdfs:label ?label ;\ + ?building .\ + ?building soton:UoSBuilding .\ + OPTIONAL {\ + ?mdf ?room .\ + ?room rdf:type ns1:Room\ + }\ +} group by ?mdf"; + + sparqlQuery(query, function(err, data) { + if (err) { + console.error("Query " + query); + console.error(err); + } + + var printerLabelByURI = {}; + + // For validation + var openDataPrinterURIs = {} + + async.map(data.results.bindings, function(result, callback) { + + var uri = result.mdf.value; + + openDataPrinterURIs[uri] = true; + + var building = result.building.value; + if ("room" in result) + var room = result.room.value; + var label = result.label.value; + + printerLabelByURI[uri] = label; + + var feature = { + type: "Feature", + properties: { + label: label, + uri: uri + } + }; + + if (uri in printers) { + feature.geometry = { + type: "Point", + coordinates: printers[uri].coordinates + }; + + feature.properties.level = parseInt(printers[uri].level, 10); + } + + if (building in buildings) { + var buildingProperties = buildings[building].properties; + + if (!('services' in buildingProperties)) { + buildingProperties.services = { mfds: [] }; + } else if (!('mfds' in buildingProperties.services)) { + buildingProperties.services.mfds = []; + } + + buildingProperties.services.mfds.push(uri); + + buildingProperties.services.mfds.sort(function(aURI, bURI) { + var textA = printerLabelByURI[aURI].toUpperCase(); + var textB = printerLabelByURI[bURI].toUpperCase(); + return (textA < textB) ? -1 : (textA > textB) ? 1 : 0; + }); + } else { + addBuildingMessage(building, "errors", "location", "unknown buildingPrinter"); + } + + callback(null, feature); + }, function(err, results) { + var printersWithLocations = 0; + + Object.keys(printers).forEach(function(uri) { + if (!(uri in openDataPrinterURIs)) { + console.err("printer " + uri + " is not known"); + } else { + printersWithLocations++; + } + }); + + console.log("finished processing printers (" + printersWithLocations + "/" + Object.keys(openDataPrinterURIs).length + ")"); + + async.filter(results, + function(printer, callback) { + callback(typeof printer !== 'undefined'); + }, + function(cleanResults) { + callback(err, cleanResults); + } + ); + }); + }); +} + +function getVendingMachines(buildings, callback) { + console.log("begin getVendingMachines"); + + var query = "PREFIX spacerel: \ +PREFIX soton: \ +PREFIX rdfs: \ +SELECT * WHERE {\ + ?uri a ;\ + rdfs:label ?label ;\ + soton:vendingMachineModel ?model ;\ + soton:vendingMachineType ?type ;\ + spacerel:within ?building .\ +}"; + + sparqlQuery(query, function(err, data) { + if (err) { + console.error("Query " + query); + console.error(err); + callback(err); + return; + } + + query = "select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as point,osm_id,vending,level,uri from planet_osm_point where ST_Contains((select ST_Union(way) from uni_site), way) and amenity='vending_machine';" + + pg.query(query, function(err, results) { + if (err) { + console.error("Query: " + query); + console.error(err); + callback(err); + return; + } + + var machinesByURI = {}; + var machines = []; + + // First, look through OSM finding the location of the vending + // machines + results.rows.forEach(function(part) { + var feature = { type: "Feature" }; + feature.geometry = JSON.parse(part.point); + delete part.point; + delete part.osm_id; + + feature.properties = part; + + machinesByURI[part.uri] = feature; + + machines.push(feature); + }); + + // Then look through the University Open Data, to find the ones OSM + // is missing, and any additional information + data.results.bindings.forEach(function(result) { + var uri = result.uri.value; + var machine; + + if (uri in machinesByURI) { + machine = machinesByURI[uri]; + + machine.properties.label = result.label.value; + } else { + machine = { type: "Feature", properties: { uri: uri, label: result.label.value } }; + + machinesByURI[uri] = machine; + + machines.push(machine); + } + + var building = result.building.value; + if (!(building in buildings)) { + if (building.indexOf("site") === -1) // building could actually be a site, query needs fixing + addBuildingMessage(building, "errors", "location", "unknown (vendingMachine)"); + } else { + var buildingProperties = buildings[building].properties; + + if (!('services' in buildingProperties)) { + buildingProperties.services = { vendingMachines: [] }; + } else if (!('vendingMachines' in buildingProperties.services)) { + buildingProperties.services.vendingMachines = []; + } + + buildingProperties.services.vendingMachines.push(uri); + } + }); + + callback(err, machines); + }); + }); +} + +// buses + +function loadBusData(callback) { + async.waterfall([ + function(callback) { + pg.query('drop table if exists uni_bus_route', function(err, results) { + callback(err); + }); + }, + function(callback) { + console.log("creating uni_bus_route"); + pg.query('create table uni_bus_route ( way geometry, name text, note text, colour text, ref text)', function(err, results) { + callback(err); + }); + }, + function(callback) { + var query = "select id,parts,members,tags from planet_osm_rels where tags @> array['type', 'route_master', 'Uni-link']"; + pg.query(query, callback); + }, + function(results, callback) { + async.map(results.rows, function(relation, callback) { + processRelation(relation, callback); + }, callback); + }, + function(routeMasters, callback) { + var stopAreaRoutes = {} // Mapping from id to stop area, also contains the route names for that stop area + + async.each(routeMasters, function(routeMaster, callback) { + async.each(routeMaster.members, function(member, callback) { + getRelation(member.ref, function(err, route) { + if (err) callback(err); + + var ways = []; + var stopAreasRoutes = {}; + + async.eachSeries(route.members, function(member /* either a stop_area, or a road */, callback) { + if (member.type === "relation") { // Then its a stop_area + // Add the stop to the list (stopAreas) + if (member.ref in stopAreaRoutes) { + if (stopAreaRoutes[member.ref].indexOf(route.tags.ref) < 0) + stopAreaRoutes[member.ref].push(route.tags.ref); + } else { + stopAreaRoutes[member.ref] = [route.tags.ref]; + } + callback(); + } else { + var query = "select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as way from planet_osm_line where osm_id = " + member.ref; + + pg.query(query, function(err, results) { + if (err) callback(err); + + ways.push(JSON.parse(results.rows[0].way).coordinates); + + callback(); + }); + } + }, function(err) { + // Now to create the route geometry + + createRouteGeometry(ways, function(err, routeCoords) { + if (err) { + console.error("geometry errors for route " + route.tags.name); + err.forEach(function(error) { + console.log(" " + error); + }); + } + + var flattenedCoords = []; + flattenedCoords = flattenedCoords.concat.apply(flattenedCoords, routeCoords); + + var colour = ('colour' in route.tags) ? route.tags.colour : routeMaster.tags.colour; + + var pgQuery = "insert into uni_bus_route values(ST_GeomFromText('LINESTRING(" + flattenedCoords.join(" ") + "'), "; + pgQuery = pgQuery + "'" + [route.tags.name, "note", colour, route.tags.ref].join("', '") + "')"; + + callback(); + /*pg.query(pgQuery, function(err, result) { + callback(err); + });*/ + }); + }); + }); + }, callback); + }, function(err) { + callback(err, stopAreaRoutes); + }); + }, + function(stopAreaRoutes, callback) { + // Now the route processing has finished, the bus stops can be created + + createBusStops(stopAreaRoutes, callback); + } + ], function(err) { + console.log("finished loadBusData"); + if (err) + console.error(err); + + callback(err); + }); +} + +function createRouteGeometry(ways, callback) { + var routeCoords = []; + + function last(way) { + return way.slice(-1)[0]; + } + + function first(way) { + return way[0]; + } + + function equal(coord1, coord2) { + return coord1[0] === coord2[0] && coord1[1] === coord2[1]; + } + + // Determine the orientation of the first way + if (equal(last(ways[0]), first(ways[1])) || equal(last(ways[0]), last(ways[1]))) { + routeCoords = ways[0]; + } else { + routeCords = ways[0].reverse(); + } + + var errors = []; + + for (var i=1; i' + room + ' is missing'; + + addBuildingToDo(building, 'rooms', roomNeeded); + } + + callback(); + }, callback); +} + +function validateBuildings(callback) { + var query = "PREFIX soton: \ +PREFIX skos: \ +SELECT * WHERE {\ + ?building soton:UoSBuilding ;\ + skos:notation ?ref\ +}"; + + sparqlQuery(query, function(err, data) { + if (err) { + console.error("Query " + query); + console.error(err); + } + + async.each(data.results.bindings, function(building, callback) { + var uri = building.building.value; + + if (!(uri in buildings)) { + addBuildingMessage(uri, "errors", "location", "unknown (validateBuildings)"); + } + + callback(); + }, function(err) { + console.log("finished validateBuildings"); + callback(err); + }); + }); +} + +function addBuildingMessage(buildingURI, severity, section, message) { + var buildingValidation; + + if (buildingURI in validationByURI) { + buildingValidation = validationByURI[buildingURI]; + } else { + buildingValidation = {todo: {}, warnings: {}, errors: {}}; + validationByURI[buildingURI] = buildingValidation; + } + + if (!(section in buildingValidation[severity])) { + buildingValidation[severity][section] = []; + } + + buildingValidation[severity][section].push(message); +} + +function addRoomMessage(roomURI, severity, section, message) { + var roomValidation; + + if (roomURI in validationByURI) { + roomValidation = validationByURI[roomURI]; + } else { + roomValidation = {todo: {}, warnings: {}, errors: {}}; + validationByURI[roomURI] = roomValidation; + } + + if (!(section in roomValidation[severity])) { + roomValidation[severity][section] = []; + } + + roomValidation[severity][section].push(message); +} diff --git a/examples/basic.html b/examples/basic.html new file mode 100644 index 0000000..161ecaa --- /dev/null +++ b/examples/basic.html @@ -0,0 +1,34 @@ + + + + Map - University of Southampton + + + + + + + + +
+ + + + + + + + diff --git a/examples/buildingsearch.html b/examples/buildingsearch.html new file mode 100644 index 0000000..7ee8f40 --- /dev/null +++ b/examples/buildingsearch.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + +
+
+ Building Search +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ Type "b12." to search for a specific building number. +
+
+ +
+
+
+ + + + + + diff --git a/examples/full.html b/examples/full.html new file mode 100644 index 0000000..02d1c96 --- /dev/null +++ b/examples/full.html @@ -0,0 +1,45 @@ + + + + Workstations - University of Southampton + + + + + + + + + + +
+ + + + + + + + + + + diff --git a/examples/index.html b/examples/index.html new file mode 100644 index 0000000..997e65d --- /dev/null +++ b/examples/index.html @@ -0,0 +1,372 @@ + + + + Leaflet Soton - Documentation + + + + + + + + + + + + + + + +

An Introduction to OpenStreetMap and the University of Southampton

+ +

OpenStreetMap

+ + + + + +
+

+ + The OpenStreetMap project was started in 2004 with the aim of + creating a free map of the world. The core, but perhaps less + obvious output from the project is the data. You can see an + example of some OSM data, represented in XML on the right. + +

+

+ + This abreviated data describes the Zepler building on Highfield + Campus. You can see in the tags, that the building name, + number, and uri are present. + +

+
+ +
+ +

Slippy Maps

+ + + + + + + + + +
+ + +

Data to Images (or Tiles)

+

+ + XML is perhaps not the best way to view the data if you want to + use if for navigation, so this data can be rendered in to + images for use in a map. Shown here is the "Standard" rendering + of part of Highfield campus. + +

+

+ + This tile has been downloaded from the OpenStreetMap tile + servers. A tileserver is normally composed of a database + (usually postgresql with postgis), a tile rendering stack + (e.g. renderd and mapnik) and a webserver (e.g. apache with + mod_tile). + +

+
+

Leaflet

+

+ + There are a few libraries that can handle displaying a "slippy" + map, the one in use here is called leaflet. Tiles like the one + shown above are combined together in to a grid that can be + moved with the mouse. + +

+

+ + The javascript used to create the map seen on the right is shown below. + +

+ +
+                var map1 = L.map('zepler-osm');
+
+                map1.setView([50.93733, -1.39779], 19);
+
+                L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
+                    maxZoom: 19
+                }).addTo(map1);
+                
+
+
+
+

Custom Maps for the University

+ + + + + +
+
+
+

Custom Rendering (sum-carto)

+

+ + Jumping back to tileservers, the rendering stack decides how to + display the data as an image. Mapnik, which is commonly used + has an xml stylesheet that it uses, this can be (and in the + case of the "Standard" OSM style, is) generated from another + CSS like language called carto. + +

+

+ + The "Standard" OSM rendering has some disadvantages for the + University. Probably the biggest is that it shows some + irelevent things, e.g. housenumbers for the surrounding + residential area, it also does not show relevent things e.g. + building numbers, cycle parking, ... + +

+

+ + To the left, you can see the same view as before, but this + time, the data has been rendered according to a different + stylesheet. Notice that the buildings number is displayed + below the name, and that some cycle parking is shown just to + the top right of the Zepler building (green P). + +

+

+ + Currently there are two tileservers hosting this University + specific tileset. Inside ECS there is + kanga-cb15g11.ecs.soton.ac.uk, which is updated daily. Outside + of ECS, there is altair.cbaines.net, which is rarely updated. + +

+
+                var map2 = L.map('sum-carto');
+
+                map2.setView([50.93733, -1.39779], 19);
+
+                L.tileLayer('http://kanga-cb15g11.ecs.soton.ac.uk/sum/{z}/{x}/{y}.png', {
+                    maxZoom: 19
+                }).addTo(map2);
+                
+
+

Interactivity

+ + + + + + + + + +
+

sum

+

+ + So far we have seen static maps, and slippy maps. But extra + interactivty can be used to enhance slippy maps with more data + where available. The University is off to a good start with + this data, and some of it can be interacted with through the + map. + +

+

+ + The sum (TODO: Find better name) library allows you to add this + interactivity to the map with ease, its mainly clientside, with + a small server side component. It handles everything from + setting up Leaflet, to advanced interactive components. + +

+

+ + Try clicking on the buildings and the bicycle parking. + +

+
+                var map3 = LS.map('sum-basic');
+                
+
+
+
+
+
+

Workstations

+

+ + One of the very useful pieces of information published by the + University is the workstation use data. On the left you can see + this being displayed on the map. + +

+

+ + As you zoom in and out, the workstation markers will group + together and split apart to keep the data visible. All markers + are also interactive and display more data in a popup when + clicked on. + +

+
+                var map4 = LS.map('sum-workstations', {
+                    workstations: true
+                });
+                
+
+

Future Experimental Features

+ + + + + +
+

Indoor Maps

+

+ + Navigating around the University is slightly different from + navigating around a city, e.g. Southampton or London. Most of + navigation for students and staff of the university involves + moving between rooms. + +

+

+ + The sum library also has an experimental indoor feature, you + can see a view of the library (level 2) on the left. + +

+
+
+
+ + + + + + + + + diff --git a/examples/indoor.html b/examples/indoor.html new file mode 100644 index 0000000..0e46409 --- /dev/null +++ b/examples/indoor.html @@ -0,0 +1,37 @@ + + + + Map - University of Southampton + + + + + + + + +
+ + + + + + + + diff --git a/examples/search.html b/examples/search.html new file mode 100644 index 0000000..85e8d80 --- /dev/null +++ b/examples/search.html @@ -0,0 +1,222 @@ + + + + + + + + + + + + +
+
+ Search +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+ Type "b12." to search for a specific building number. +
+
+ +
+
+
+ + + + + + diff --git a/examples/workstations.html b/examples/workstations.html new file mode 100644 index 0000000..819c41b --- /dev/null +++ b/examples/workstations.html @@ -0,0 +1,39 @@ + + + + Workstations - University of Southampton + + + + + + + + + +
+ + + + + + + + + diff --git a/examples/zepler.html b/examples/zepler.html new file mode 100644 index 0000000..4caf762 --- /dev/null +++ b/examples/zepler.html @@ -0,0 +1,44 @@ + + + + Zepler (Building 59) + + + + + + + + + +
+ + + + + + + + + diff --git a/resources/images/candy.png b/resources/images/candy.png new file mode 100644 index 0000000..6862980 Binary files /dev/null and b/resources/images/candy.png differ diff --git a/resources/images/coffee.png b/resources/images/coffee.png new file mode 100644 index 0000000..47da029 Binary files /dev/null and b/resources/images/coffee.png differ diff --git a/resources/images/loader.gif b/resources/images/loader.gif new file mode 100644 index 0000000..d3ef195 Binary files /dev/null and b/resources/images/loader.gif differ diff --git a/resources/images/locate.png b/resources/images/locate.png new file mode 100644 index 0000000..14faeb7 Binary files /dev/null and b/resources/images/locate.png differ diff --git a/resources/images/locate.svg b/resources/images/locate.svg new file mode 100644 index 0000000..1b5d318 --- /dev/null +++ b/resources/images/locate.svg @@ -0,0 +1,383 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/resources/images/locate_alt.png b/resources/images/locate_alt.png new file mode 100644 index 0000000..acebf30 Binary files /dev/null and b/resources/images/locate_alt.png differ diff --git a/resources/images/locate_alt.svg b/resources/images/locate_alt.svg new file mode 100644 index 0000000..63ccfb5 --- /dev/null +++ b/resources/images/locate_alt.svg @@ -0,0 +1,493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/locate_touch.png b/resources/images/locate_touch.png new file mode 100644 index 0000000..9669ff4 Binary files /dev/null and b/resources/images/locate_touch.png differ diff --git a/resources/images/locate_touch_alt.png b/resources/images/locate_touch_alt.png new file mode 100644 index 0000000..a3b5196 Binary files /dev/null and b/resources/images/locate_touch_alt.png differ diff --git a/resources/images/logos/stags-head.png b/resources/images/logos/stags-head.png new file mode 100755 index 0000000..9b65634 Binary files /dev/null and b/resources/images/logos/stags-head.png differ diff --git a/resources/images/logos/stags-head2.png b/resources/images/logos/stags-head2.png new file mode 100755 index 0000000..e61552b Binary files /dev/null and b/resources/images/logos/stags-head2.png differ diff --git a/resources/images/logos/susu-cafe.png b/resources/images/logos/susu-cafe.png new file mode 100755 index 0000000..49a7e86 Binary files /dev/null and b/resources/images/logos/susu-cafe.png differ diff --git a/resources/images/logos/susu-cafe2.png b/resources/images/logos/susu-cafe2.png new file mode 100755 index 0000000..1af5a40 Binary files /dev/null and b/resources/images/logos/susu-cafe2.png differ diff --git a/resources/images/logos/susu-shop.png b/resources/images/logos/susu-shop.png new file mode 100755 index 0000000..f6820a4 Binary files /dev/null and b/resources/images/logos/susu-shop.png differ diff --git a/resources/images/logos/susu-shop2.png b/resources/images/logos/susu-shop2.png new file mode 100755 index 0000000..6bbfb90 Binary files /dev/null and b/resources/images/logos/susu-shop2.png differ diff --git a/resources/images/logos/susu.png b/resources/images/logos/susu.png new file mode 100755 index 0000000..44aa056 Binary files /dev/null and b/resources/images/logos/susu.png differ diff --git a/resources/images/logos/susu2.png b/resources/images/logos/susu2.png new file mode 100755 index 0000000..d9942f2 Binary files /dev/null and b/resources/images/logos/susu2.png differ diff --git a/resources/images/logos/the-bridge.png b/resources/images/logos/the-bridge.png new file mode 100755 index 0000000..e803595 Binary files /dev/null and b/resources/images/logos/the-bridge.png differ diff --git a/resources/images/logos/the-bridge2.png b/resources/images/logos/the-bridge2.png new file mode 100755 index 0000000..b5ad5c8 Binary files /dev/null and b/resources/images/logos/the-bridge2.png differ diff --git a/resources/images/printer.png b/resources/images/printer.png new file mode 100644 index 0000000..1a5ee39 Binary files /dev/null and b/resources/images/printer.png differ diff --git a/resources/images/search-icon-mobile.png b/resources/images/search-icon-mobile.png new file mode 100644 index 0000000..920fa27 Binary files /dev/null and b/resources/images/search-icon-mobile.png differ diff --git a/resources/images/search-icon.png b/resources/images/search-icon.png new file mode 100644 index 0000000..b51c165 Binary files /dev/null and b/resources/images/search-icon.png differ diff --git a/resources/images/spinner.gif b/resources/images/spinner.gif new file mode 100644 index 0000000..aa70283 Binary files /dev/null and b/resources/images/spinner.gif differ diff --git a/resources/images/toilets-f.png b/resources/images/toilets-f.png new file mode 100644 index 0000000..4117ab4 Binary files /dev/null and b/resources/images/toilets-f.png differ diff --git a/resources/images/toilets-m.png b/resources/images/toilets-m.png new file mode 100644 index 0000000..93cdbfa Binary files /dev/null and b/resources/images/toilets-m.png differ diff --git a/resources/images/toilets.png b/resources/images/toilets.png new file mode 100644 index 0000000..17ccfd8 Binary files /dev/null and b/resources/images/toilets.png differ diff --git a/resources/images/toilets_disability.png b/resources/images/toilets_disability.png new file mode 100644 index 0000000..240539c Binary files /dev/null and b/resources/images/toilets_disability.png differ diff --git a/resources/images/workstation-big.png b/resources/images/workstation-big.png new file mode 100644 index 0000000..090d182 Binary files /dev/null and b/resources/images/workstation-big.png differ diff --git a/resources/images/workstation-closed.png b/resources/images/workstation-closed.png new file mode 100644 index 0000000..d8ff7b7 Binary files /dev/null and b/resources/images/workstation-closed.png differ diff --git a/resources/images/workstation-group.png b/resources/images/workstation-group.png new file mode 100644 index 0000000..d334886 Binary files /dev/null and b/resources/images/workstation-group.png differ diff --git a/resources/images/workstation.png b/resources/images/workstation.png new file mode 100644 index 0000000..a60de27 Binary files /dev/null and b/resources/images/workstation.png differ diff --git a/resources/leaflet b/resources/leaflet new file mode 160000 index 0000000..bcf370b --- /dev/null +++ b/resources/leaflet @@ -0,0 +1 @@ +Subproject commit bcf370b5be176bd6d23febb003dc8abcd2da5f35 diff --git a/resources/leaflet-hash b/resources/leaflet-hash new file mode 160000 index 0000000..d73fc84 --- /dev/null +++ b/resources/leaflet-hash @@ -0,0 +1 @@ +Subproject commit d73fc84d8cac7f1ea7068e4fe1dcf70f93791837 diff --git a/resources/leaflet-locatecontrol b/resources/leaflet-locatecontrol new file mode 160000 index 0000000..1c3cd90 --- /dev/null +++ b/resources/leaflet-locatecontrol @@ -0,0 +1 @@ +Subproject commit 1c3cd90e5f381d1cb46ca7c2efc13c4b2f0e6ca3 diff --git a/resources/leaflet-markercluster b/resources/leaflet-markercluster new file mode 160000 index 0000000..6e9ffc4 --- /dev/null +++ b/resources/leaflet-markercluster @@ -0,0 +1 @@ +Subproject commit 6e9ffc45ec9655f8dc9fefa4739fb4e6e7a4fa0b diff --git a/resources/mfd-location b/resources/mfd-location new file mode 160000 index 0000000..fe7f0b1 --- /dev/null +++ b/resources/mfd-location @@ -0,0 +1 @@ +Subproject commit fe7f0b1e4485347eb995f36db80ba62965317fbf diff --git a/resources/syntaxhighlighter b/resources/syntaxhighlighter new file mode 160000 index 0000000..841f531 --- /dev/null +++ b/resources/syntaxhighlighter @@ -0,0 +1 @@ +Subproject commit 841f53168059d89720e26bde2664291961d06a0d diff --git a/src/leaflet-soton.css b/src/leaflet-soton.css new file mode 100644 index 0000000..2bf18c2 --- /dev/null +++ b/src/leaflet-soton.css @@ -0,0 +1,275 @@ +.ls-content-table a { + text-decoration: none; +} + +.ls-popup-title { + font: 25px/16px Arial, Helvetica, sans-serif; +} + +.ls-levelselector { + line-height: 18px; + color: #555; + padding: 6px 8px; + font: 14px/16px Arial, Helvetica, sans-serif; + background: white; + background: rgba(255,255,255,0.8); + box-shadow: 0 0 15px rgba(0,0,0,0.2); + border-radius: 5px; +} + +.ls-workstation-marker { + text-align: center; +} + +.ls-room-marker { + text-align: center; +} + +.ls-workstationicon { + width: 66px; + height: 32px; + text-align: center; + font-size: 23px; +} + +#ls-dynamicContentWrapper { + background: white; + min-height: 100%; + width: 100%; + z-index: 2000; + position: absolute; + padding-left: 5px; + padding-top: 5px; + padding-right: 5px; + left: 0px; + top: 0px; + display: none; +} + +#ls-dynamicContent { + +} + +.ls-btn { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + padding: 4px 10px 4px; + margin-bottom: 0; + font-size: 13px; + line-height: 18px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + background-color: #f5f5f5; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(top, #ffffff, #e6e6e6); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + border: 1px solid #cccccc; + border-bottom-color: #b3b3b3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + cursor: pointer; + *margin-left: .3em; +} +.ls-btn:hover, +.ls-btn:active, +.ls-btn.active, +.ls-btn.disabled, +.ls-btn[disabled] { + background-color: #e6e6e6; +} +.ls-btn:active, +.ls-btn.active { + background-color: #cccccc \9; +} +.ls-btn:first-child { + *margin-left: 0; +} +.ls-btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -ms-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} +.ls-btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.ls-btn.active, +.ls-btn:active { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + outline: 0; +} + +.ls-btn-group .ls-btn { + position: relative; + float: left; + margin-left: -1px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.ls-btn-group .ls-btn:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; + border-top-left-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + border-bottom-left-radius: 4px; +} +.ls-btn-group .ls-btn:last-child, +.ls-btn-group .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; + border-bottom-right-radius: 4px; +} +.ls-btn-group .ls-btn.large:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 6px; + -moz-border-radius-topleft: 6px; + border-top-left-radius: 6px; + -webkit-border-bottom-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + border-bottom-left-radius: 6px; +} +.ls-btn-group .ls-btn.large:last-child, +.ls-btn-group .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + -moz-border-radius-topright: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + -moz-border-radius-bottomright: 6px; + border-bottom-right-radius: 6px; +} +.ls-btn-group .ls-btn:hover, +.ls-btn-group .ls-btn:focus, +.ls-btn-group .ls-btn:active, +.ls-btn-group .ls-btn.active { + z-index: 2; +} + +.ls-nav { + margin-left: 0; + padding: 0; + margin-bottom: 18px; + list-style: none; +} +.ls-nav > li > a { + text-decoration: none; + display: block; + font-size: medium +} +.ls-nav > li > a:hover { + background-color: #eeeeee; +} +.ls-nav .ls-nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 18px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} +.ls-nav li + .ls-nav-header { + margin-top: 9px; +} +.ls-nav-list { + padding-left: 15px; + padding-right: 15px; + margin-bottom: 0; +} +.ls-nav-tabs:before, +.ls-nav-pills:before, +.ls-nav-tabs:after, +.ls-nav-pills:after { + display: table; + content: ""; +} +.ls-nav-tabs:after, +.ls-nav-pills:after { + clear: both; +} +.ls-nav-tabs > li, +.ls-nav-pills > li { + float: left; +} +.ls-nav-tabs > li > a, +.ls-nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} +.ls-nav-tabs { + border-bottom: 1px solid #ddd; +} +.ls-nav-tabs > li { + margin-bottom: -1px; +} +.ls-nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 18px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.ls-nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} +.ls-nav-tabs > .active > a, +.ls-nav-tabs > .active > a:hover { + color: #555555; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; + cursor: default; +} +.ls-nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.ls-nav-pills > .active > a, +.ls-nav-pills > .active > a:hover { + color: #ffffff; + background-color: #0088cc; +} 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 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) { + + // 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 = '
'; + + var freeSeats = workstationData[partWorkstation.feature].free_seats; + + workstationIcon += freeSeats + "
"; + + 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' + 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 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; iRoutes: \ + <% each(properties.routes, function(route) { %>\ + <%= route.ref %>\ + <% }); %>\ + \ + \ + \ + \*/ + + /*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 = '
'; + + 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 + "
"; + + openIcon.html = html; + + L.setOptions(this, openIcon); + } else { + html += ""; + + 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; + }; +} -- cgit v1.2.3