#!/usr/bin/env nodejs /* * 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 config = require("./config.json"); var library_data = require("./resources/hartley-library-map-data/data.json"); 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.waterfall([ // Creates the database tables that can be created with a simple query createTables, // Get the data from these tables createCollections, function(collections, callback) { async.each(collections.pointsOfService.features, function(feature, callback) { if ("uri" in feature.properties) { async.parallel([ function(callback) { getOfferings(feature.properties.uri, function(err, offerings) { feature.properties.offerings = offerings; callback(); }); }, function(callback) { getDescription(feature.properties.uri, function(err, description) { feature.properties.description = description; callback(); }); }], callback ); } else { console.warn("missing uri for point of service"); callback(); } }, function(err) { callback(null, collections); }); }, // Now the basic collections have been created, handle the more complicated // ones: // - busStops // - busRoutes // - buildingParts // - workstations // // Extracting the data for these is a bit harder than the simpler // collections. function(collections, callback) { // Create an object with the buildings in for easy lookup var buildings = {}; collections.buildings.features.forEach(function(building) { if ("uri" in building.properties) { buildings[building.properties.uri] = building; } }); createBuildingParts(buildings, function(err, buildingParts, workstations) { collections.buildingParts = buildingParts; async.parallel([ function(callback) { getPrinters(buildings, function(err, features) { collections.multiFunctionDevices = { type: "FeatureCollection", features: features }; callback(); }); }, function(callback) { getVendingMachines(buildings, function(err, features) { collections.vendingMachines = { type: "FeatureCollection", features: features }; callback(); }); }, function(callback) { getUniWorkstations(workstations, function(err, workstations) { collections.workstations = workstations; callback(err); }); } ], function(err) { getBuildingImages(buildings, function(err) { getLibraryData(library_data, function(err, features) { collections.buildingParts.features.push.apply(collections.buildingParts.features, features); callback(err, collections); }); }); }); }); }, loadBusData ], function(err, collections){ if (err) { console.error(err); process.exit(1); } console.info("ending database connection"); pgql.end(); writeDataFiles(collections, function() { Object.keys(validationByURI).sort().forEach(function(uri) { if ("location" in validationByURI[uri].errors) { console.warn(uri + " location unknown"); } }); console.info("complete"); process.exit(0); }); }); }); // This code handles creating the basic collections, that is: // - buildings // - parking // - bicycleParking // - sites // - busStops // // It is done this way, as this is the data that has to be loaded in to the // database for the renderer to work. function createTables(callback) { var tableSelects = { site: "select way,name,loc_ref,uri,amenity,landuse \ from planet_osm_polygon \ where uri like 'http://id.southampton.ac.uk/site/%'", 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) or uri like 'http://id.southampton.ac.uk/building/%') 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.info("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.info("finished creating table " + tableName); } callback(err); }); }); } function createCollections(callback) { var collectionQueries = { buildings: 'select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as polygon,\ ST_AsText(ST_Transform(ST_Centroid(way), 4326)) as center,\ 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,\ ST_AsText(ST_Transform(ST_Centroid(way), 4326)) as center,\ name,loc_ref,uri from uni_site', pointsOfService: "select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as polygon,ST_AsText(ST_Transform(ST_Centroid(way), 4326)) as center,name,shop,amenity,uri from planet_osm_polygon where (amenity in ('cafe', 'bar', 'restaurant') or shop in ('kiosk', 'convenience')) and ST_Contains((select ST_Union(way) from uni_site), way);" }; 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(); center = center.map(parseFloat); feature.properties.center = center; } callback(err, feature); }, callback); }); } function getBuildingImages(buildings, callback) { console.info("getting building images"); async.each(Object.keys(buildings), function(uri, callback) { getImagesFor(uri, function(err, images) { buildings[uri].properties.images = images; callback(err); }); }, callback); } // buildingParts function processBuildingParts(buildingParts, callback) { var buildingPartsByURI = {}; var workstations = {}; async.each(buildingParts, function(part, callback) { if (part.properties.buildingpart === "room") { if ("uri" in part.properties) { buildingPartsByURI[part.properties.uri] = part; var expectedRef = part.properties.uri.split("/").slice(-1)[0].split("-")[1]; if ("ref" in part.properties) { if (part.properties.ref !== expectedRef) { console.warn("Unexpected ref \"" + part.properties.ref + "\" for room " + part.properties.uri); } } else { // does it look like a ref (is the first character a number) if (!isNaN(expectedRef.slice(0, 1))) { console.warn("Missing ref \"" + expectedRef + "\" for room " + part.properties.uri); } } async.parallel([ function(callback) { findRoomFeatures(part, callback); }, function(callback) { findRoomContents(part, workstations, callback); }, function(callback) { getRecommendedEntrances(part, callback); }, function(callback) { findRoomImages(part, callback); }], callback); } else { console.warn("room has no URI " + linkToLoc(part.properties.center)); callback(); } } else { callback(); } }, function(err) { // list such that it fits within async's pattern callback(err, [buildingPartsByURI, workstations]); }); } function linkToLoc(loc, reverse) { if (reverse) loc = loc.slice(0).reverse(); return "http://cbaines.net/sum/#1/22/" + loc.join('/'); } function getPartToLevelMap(buildingRelations, buildings, callback) { var osmIDToLevels = {}; var osmIDToBuilding = {}; // Process level relations async.each(buildingRelations, function(buildingRelation, callback) { getLevelRelations(buildingRelation, function(err, levelRelations) { levelRelations.forEach(function(level) { for (var i=0; i 1) { console.log(parts.length + " parts"); console.log("initial possible levels " + JSON.stringify(possibleLevels)); }*/ for (var i=1; i ARRAY[osm_id];"; pg.query(query, function(err, results) { if (err) { console.error("Query: " + query); console.error(err); callback(err); return; } async.map(results.rows, function(part, callback) { var feature = { type: "Feature", id: part.osm_id, geometry: JSON.parse(part.point), properties: {} }; callback(null, feature); }, callback); }); } function getBuildingEntrances(callback) { var query = "select osm_id, ST_AsGeoJSON(ST_Transform(way, 4326), 10) as polygon from planet_osm_point where ST_Contains((select ST_Union(way) from uni_site), way) and entrance is not null"; pg.query(query, function(err, results) { if (err) { console.error("Query: " + query); console.error(err); callback(err); return; } async.map(results.rows, function(part, callback) { var feature = { type: "Feature", id: part.osm_id, properties: { buildingpart: "entrance" } }; feature.geometry = JSON.parse(part.polygon); callback(null, feature); }, callback); }); } function getBuildingParts(callback) { var query = "select ST_AsGeoJSON(ST_Transform(way, 4326), 10) as polygon,ST_AsText(ST_Transform(ST_Centroid(way), 4326)) as center,osm_id,name,buildingpart,\"buildingpart:verticalpassage\",ref,uri,amenity,unisex,male,female from planet_osm_polygon where buildingpart is not null"; pg.query(query, function(err, results) { if (err) { console.error("Query: " + query); console.error(err); callback(err); return; } async.map(results.rows, function(part, callback) { var feature = {type: "Feature", id: part.osm_id}; feature.geometry = JSON.parse(part.polygon); delete part.polygon; delete part.osm_id; feature.properties = part; 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; } callback(null, feature); }, callback); }); } function getBuildingRelations(callback) { var query = "select id,parts,members,tags from planet_osm_rels where (tags[1] = 'type' and tags[2] = 'building') or (tags[3] = 'type' and tags[4] = 'building')"; pg.query(query, function(err, results) { if (err) { console.error("Query: " + query); console.error(err); callback(err); return; } async.map(results.rows, function(relation, callback) { processRelation(relation, callback); }, callback); }); } function getLevelRelations(buildingRelation, callback) { var refs = []; for (var i=0; i"; query += ')}'; sparqlQuery(query, function(err, data) { if (err) { console.error("error in findRoomFeatures"); console.error("query " + query); console.error(err); } room.properties.features = []; data.results.bindings.forEach(function(feature) { if ('error-message' in feature) { console.error("error in findRoomFeatures"); console.error(JSON.stringify(feature)); console.error("query:\n" + query); return; } room.properties.features.push({ feature: feature.feature.value, label: feature.label.value }); }); callback(); }); } function getPortals(callback) { var query = "PREFIX rdfs: \ PREFIX portals: \ PREFIX geo: \ SELECT * WHERE {\ ?portal a portals:BuildingEntrance;\ portals:connectsBuilding ?building;\ OPTIONAL {\ ?portal rdfs:comment ?comment .\ ?portal rdfs:label ?label .\ ?portal geo:lat ?lat .\ ?portal geo:long ?long .\ ?portal portals:connectsFloor ?floor\ }\ }" sparqlQuery(query, function(err, data) { if (err) { console.error("error in getPortals"); console.error("query " + query); console.error(err); } portals = []; data.results.bindings.forEach(function(portal) { if ('error-message' in portal) { console.error("error in portals"); console.error(JSON.stringify(feature)); console.error("query:\n" + query); return; } var obj = { uri: portal.portal.value, building: portal.building.value, } if ("floor" in portal) { obj.floor = portal.floor.value; } if ("label" in portal) { obj.label = portal.label.value; } if ("comment" in portal) { obj.comment = portal.comment.value; } if ("lat" in portal) { obj.lat = portal.lat.value; } if ("long" in portal) { obj.lon = portal.long.value; } portals.push(obj); }); callback(null, portals); }); } function getRecommendedEntrances(part, callback) { var query = "PREFIX rdfs: \ PREFIX portals: \ PREFIX geo: \ SELECT ?portal WHERE {\ ?uri portals:recommendedBuildingEntrance ?portal\ FILTER (\ ?uri = \ )\ }"; query = query.replace("URI", part.properties.uri); sparqlQuery(query, function(err, data) { if (err) { console.error("error in getRecommended Entrance"); console.error("query " + query); console.error(err); } part.properties.recommendedEntrances = portals = []; data.results.bindings.forEach(function(portal) { if ('error-message' in portal) { console.error("error in portals"); console.error(JSON.stringify(feature)); console.error("query:\n" + query); return; } portals.push(portal.portal.value); }); callback(null, portals); }); } // 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("error in getUniWorkstations"); console.error(err); console.error("query " + query); } 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("error in getUniWorkstations"); console.error(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); }); }); } function getPrinters(buildings, callback) { console.info("begining create printers"); var query = "PREFIX spacerel: \ PREFIX soton: \ PREFIX rdfs: \ PREFIX ns1: \ PREFIX rdf: \ SELECT DISTINCT * WHERE {\ ?mdf a ;\ rdfs:label ?label ;\ ?building .\ ?building soton:UoSBuilding .\ OPTIONAL {\ ?mdf ?room .\ ?room rdf:type ns1:Room\ }\ }"; sparqlQuery(query, function(err, data) { if (err) { console.error("error in getPrinters"); console.error(err); console.error("query " + query); } var printerLabelByURI = {}; // For validation var openDataPrinterURIs = {} var printersWithLocations = 0; async.map(data.results.bindings, function(result, callback) { if ('error-message' in result) { console.error("error in getPrinters"); console.error(result); console.error("query " + query); callback(); return; } 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 } }; var printers = require("./resources/mfd-location/data.json"); var printersByURI = {} printers.features.forEach(function(printer) { printersByURI[printer.properties.uri] = printer; }); if (uri in printersByURI) { printer = printersByURI[uri]; feature.geometry = printer.geometry; feature.properties.level = printer.properties.level; printersWithLocations += 1; } else { console.error("error printer " + uri + " is not known"); } 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) { console.info("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.info("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); }); }); } // busStops and busRoutes function processRoute(route, routeMaster, stopAreaRoutes, callback) { var ways = []; var stopRefs = []; 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]; } stopRefs.push(member.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 getRelations(stopRefs, function(err, areas) { var stopURIs = areas.map(function(area) { var uri = area.tags.uri; if (uri.indexOf("http://transport.data.gov.uk/id/stop-point/") === 0) { uri = "http://id.southampton.ac.uk/bus-stop/" + uri.slice(43); } else { console.warn("Unrecognised bus stop uri " + uri); } return uri; }); createRouteGeometry(ways, function(err, routeCoords) { if (err) { console.error("geometry errors for route " + route.tags.name); err.forEach(function(error) { console.error(" " + error); }); } var busRoute = { type: "Feature", geometry: { type: "LineString", coordinates: routeCoords }, properties: { name: route.tags.name, ref: route.tags.ref, stops: stopURIs } } if ('colour' in route.tags) { busRoute.properties.colour = route.tags.colour; } if (routeMaster !== null) { busRoute.properties.routeMaster = routeMaster.tags.ref; if (!('colour' in route.tags) && 'colour' in routeMaster.tags) { busRoute.properties.colour = routeMaster.tags.colour; } } callback(null, busRoute); }); }); }); } function loadBusData(collections, callback) { var stopAreaRoutes = {} // Mapping from id to stop area, also contains the route names for that stop area async.waterfall([ 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) { collections.busRoutes = { type: "FeatureCollection", features: [] }; async.eachSeries(routeMasters, function(routeMaster, callback) { async.eachSeries(routeMaster.members, function(member, callback) { // Pull in the route in the route master getRelation(member.ref, function(err, route) { if (err) callback(err); processRoute(route, routeMaster, stopAreaRoutes, function(err, feature) { collections.busRoutes.features.push(feature); callback(err); }); }); }, callback); }, callback); }, // Now look at the individual routes that are not in route masters function(callback) { var query = "select id,parts,members,tags from planet_osm_rels where tags @> array['type', 'route', 'Uni-link']"; pg.query(query, callback); }, function(results, callback) { async.map(results.rows, function(relation, callback) { processRelation(relation, callback); }, callback); }, function(routes, callback) { async.eachSeries(routes, function(route, callback) { processRoute(route, null, stopAreaRoutes, function(err, feature) { // Check if this route is a duplicate for (var i in collections.busRoutes.features) { var route = collections.busRoutes.features[i]; if (route.properties.name === feature.properties.name) { callback(err); return; } } collections.busRoutes.features.push(feature); callback(err); }); }, callback); }, // Now the route processing has finished, the bus stops can be created function(callback) { createBusStops(stopAreaRoutes, callback); } ], function(err, busStops) { collections.busStops = busStops; // Now remove all but the longest U1C route... var longestRoute = 0; for (var i in collections.busRoutes.features) { var route = collections.busRoutes.features[i]; if (route.properties.ref !== "U1C") { continue; } var stops = route.properties.stops.length; console.log(stops); if (stops > longestRoute) { longestRoute = stops; } } console.log("longest route " + longestRoute); i = collections.busRoutes.features.length; while (i--) { route = collections.busRoutes.features[i]; if (route.properties.ref !== "U1C") { continue; } var stops = route.properties.stops.length; if (stops !== longestRoute) { console.log("removing " + i); collections.busRoutes.features.splice(i, 1); } } console.info("finished loadBusData"); if (err) console.error(err); callback(err, collections); }); } function createRouteGeometry(ways, callback) { var routeCoords = []; var errors = []; //console.log(JSON.stringify(ways.slice(2)), null, 4); 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]; } // If the first way end joins with the 2nd way start or end, leave it as is if (!(equal(last(ways[0]), first(ways[1])) || equal(last(ways[0]), last(ways[1])))) { ways[0].reverse(); // Check if this reversed starting way works if (!(equal(last(ways[0]), first(ways[1])) || equal(last(ways[0]), last(ways[1])))) { errors.push("cannot determine correct alignment of first way"); } } // Add a clone such that the pop in the following loop does not modify the // original array routeCoords = ways[0].slice(0); 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.info("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); }