var request = require("request-promise"); var config = require("../config"); var Promise = require("promise"); var cheerio = require("cheerio"); var zlib = require("zlib"); var compressjs = require("compressjs"); request = request.defaults({ gzip: true, headers: { 'User-Agent': config.userAgent } }); var shortLinkCharArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_@"; var nameFinderUrl = "https://nominatim.openstreetmap.org/search"; var limit = 25; var stateAbbr = { "us" : { "alabama":"AL","alaska":"AK","arizona":"AZ","arkansas":"AR","california":"CA","colorado":"CO","connecticut":"CT", "delaware":"DE","florida":"FL","georgia":"GA","hawaii":"HI","idaho":"ID","illinois":"IL","indiana":"IN","iowa":"IA", "kansas":"KS","kentucky":"KY","louisiana":"LA","maine":"ME","maryland":"MD","massachusetts":"MA","michigan":"MI", "minnesota":"MN","mississippi":"MS","missouri":"MO","montana":"MT","nebraska":"NE","nevada":"NV","new hampshire":"NH", "new jersey":"NJ","new mexico":"NM","new york":"NY","north carolina":"NC","north dakota":"ND","ohio":"OH","oklahoma":"OK", "oregon":"OR","pennsylvania":"PA","rhode island":"RI","south carolina":"SC","south dakota":"SD","tennessee":"TN", "texas":"TX","utah":"UT","vermont":"VT","virginia":"VA","washington":"WA","west virginia":"WV","wisconsin":"WI","wyoming":"WY" }, "it" : { "agrigento":"AG","alessandria":"AL","ancona":"AN","aosta":"AO","arezzo":"AR","ascoli piceno":"AP","asti":"AT", "avellino":"AV","bari":"BA","barletta":"BT","barletta-andria-trani":"BT","belluno":"BL","benevento":"BN", "bergamo":"BG","biella":"BI","bologna":"BO","bolzano":"BZ","brescia":"BS","brindisi":"BR","cagliari":"CA", "caltanissetta":"CL","campobasso":"CB","carbonia-iglesias":"CI","caserta":"CE","catania":"CT","catanzaro":"CZ", "chieti":"CH","como":"CO","cosenza":"CS","cremona":"CR","crotone":"KR","cuneo":"CN","enna":"EN","fermo":"FM", "ferrara":"FE","firenze":"FI","foggia":"FG","forli-cesena":"FC","frosinone":"FR","genova":"GE","gorizia":"GO", "grosseto":"GR","imperia":"IM","isernia":"IS","la spezia":"SP","l'aquila":"AQ","latina":"LT","lecce":"LE", "lecco":"LC","livorno":"LI","lodi":"LO","lucca":"LU","macerata":"MC","mantova":"MN","massa e carrara":"MS", "matera":"MT","medio campidano":"VS","messina":"ME","milano":"MI","modena":"MO","monza e brianza":"MB", "napoli":"NA","novara":"NO","nuoro":"NU","ogliastra":"OG","olbia-tempio":"OT","oristano":"OR","padova":"PD", "palermo":"PA","parma":"PR","pavia":"PV","perugia":"PG","pesaro e urbino":"PU","pescara":"PE","piacenza":"PC", "pisa":"PI","pistoia":"PT","pordenone":"PN","potenza":"PZ","prato":"PO","ragusa":"RG","ravenna":"RA", "reggio calabria":"RC","reggio emilia":"RE","rieti":"RI","rimini":"RN","roma":"RM","rovigo":"RO","salerno":"SA", "sassari":"SS","savona":"SV","siena":"SI","siracusa":"SR","sondrio":"SO","taranto":"TA","teramo":"TE","terni":"TR", "torino":"TO","trapani":"TP","trento":"TN","treviso":"TV","trieste":"TS","udine":"UD","varese":"VA","venezia":"VE", "verbano":"VB","verbano-cusio-ossola":"VB","vercelli":"VC","verona":"VR","vibo valentia":"VV","vicenza":"VI","viterbo":"VT" }, "ca" : { "ontario":"ON","quebec":"QC","nova scotia":"NS","new brunswick":"NB","manitoba":"MB","british columbia":"BC", "prince edward island":"PE","saskatchewan":"SK","alberta":"AB","newfoundland and labrador":"NL" }, "au" : { "australian capital territory":"ACT","jervis bay territory":"JBT","new south wales":"NSW","northern territory":"NT", "queensland":"QLD","south australia":"SA","tasmania":"TAS","victoria":"VIC","western australia":"WA" } }; function find(query, loadUrls) { return Promise.resolve().then(function() { query = query.replace(/^\s+/, "").replace(/\s+$/, ""); var lonlat = isLonLatQuery(query); if(lonlat) { return [ { lat: lonlat.lat, lon : lonlat.lon, type : "coordinates", short_name: lonlat.lat + ", " + lonlat.lon, display_name : lonlat.lat + ", " + lonlat.lon, zoom: lonlat.zoom, icon: "https://nominatim.openstreetmap.org/images/mapicons/poi_place_city.p.20.png" } ]; } if(loadUrls) { var m = query.match(/^(node|way|relation)\s+(\d+)$/); if(m) return loadUrl("https://api.openstreetmap.org/api/0.6/" + m[1] + "/" + m[2] + (m[1] != "node" ? "/full" : ""), true); m = query.match(/^trace\s+(\d+)$/); if(m) return loadUrl("https://www.openstreetmap.org/trace/" + m[1] + "/data"); if(query.match(/^https?:\/\//)) return loadUrl(query); } return request({ url: nameFinderUrl + "?format=jsonv2&polygon_geojson=1&addressdetails=1&namedetails=1&limit=" + encodeURIComponent(limit) + "&extratags=1&q=" + encodeURIComponent(query), json: true }).then(function(body) { if(!body) throw "Invalid response from name finder."; var results = [ ]; body.forEach(function(result) { var displayName = makeDisplayName(result); results.push({ short_name: displayName.split(',')[0], display_name: displayName, boundingbox: result.boundingbox, lat: result.lat, lon: result.lon, extratags: result.extratags, geojson: result.geojson, icon: result.icon || "https://nominatim.openstreetmap.org/images/mapicons/poi_place_city.p.20.png", type: result.type == "yes" ? result.category : result.type, osm_id: result.osm_id, osm_type: result.osm_type }); }); return results; }); }); } /** * Tries to format a search result in a readable way according to the address notation habits in * the appropriate country. * @param result {Object} A place object as returned by Nominatim * @return {String} A readable name for the search result */ function makeDisplayName(result) { // See http://en.wikipedia.org/wiki/Address_%28geography%29#Mailing_address_format_by_country for // address notation guidelines var type = result.type; var name = result.namedetails.name; var countryCode = result.address.country_code; var road = result.address.road; var housenumber = result.address.house_number; var suburb = result.address.town || result.address.suburb || result.address.village || result.address.hamlet || result.address.residential; var postcode = result.address.postcode; var city = result.address.city; var county = result.address.county; var state = result.address.state; var country = result.address.country; if([ "road", "residential", "town", "suburb", "village", "hamlet", "residential", "city", "county", "state" ].indexOf(type) != -1) name = ""; if(!city && suburb) { city = suburb; suburb = ""; } if(road) { switch(countryCode) { case "pl": road = "ul. "+road; break; case "ro": road = "str. "+road; break; } } // Add house number to road if(road && housenumber) { switch(countryCode) { case "ar": case "at": case "ca": case "de": case "hr": case "cz": case "dk": case "fi": case "is": case "il": case "it": case "nl": case "no": case "pe": case "pl": case "sk": case "si": case "se": case "tr": road += " "+housenumber; break; case "be": case "es": road += ", "+housenumber; break; case "cl": road += " N° "+housenumber; break; case "hu": road += " "+housenumber+"."; break; case "id": road += " No. "+housenumber; break; case "my": road = "No." +housenumber+", "+road; break; case "ro": road += ", nr. "+road; break; case "au": case "fr": case "hk": case "ie": case "in": case "nz": case "sg": case "lk": case "tw": case "gb": case "us": default: road += housenumber+" "+road; break; } } // Add postcode and districts to city switch(countryCode) { case "ar": if(postcode && city) city = postcode+", "+city; else if(postcode) city = postcode; break; case "at": case "ch": case "de": if(city) { if(suburb) city += "-"+(suburb); suburb = null; if(type == "suburb" || type == "residential") type = "city"; if(postcode) city = postcode+" "+city; } break; case "be": case "hr": case "cz": case "dk": case "fi": case "fr": case "hu": case "is": case "il": case "my": case "nl": case "no": case "sk": case "si": case "es": case "se": case "tr": if(city && postcode) city = postcode+" "+city; break; case "au": case "ca": case "us": if(city && state) { var thisStateAbbr = stateAbbr[countryCode][state.toLowerCase()]; if(thisStateAbbr) { city += " "+thisStateAbbr; state = null; } } if(city && postcode) city += " "+postcode; else if(postcode) city = postcode; break; case "it": if(city) { if(county) { var countyAbbr = stateAbbr.it[county.toLowerCase().replace(/ì/g, "i")]; if(countyAbbr) { city += " ("+countyAbbr+")"; county = null; } } if(postcode) city = postcode+" "+city; } break; case "ro": if(city && county) { city += ", jud. "+county; county = null; } if(city && postcode) city += ", "+postcode; break; case "cl": case "hk": // Postcode rarely/not used case "ie": case "in": case "id": case "nz": case "pe": case "sg": case "lk": case "tw": case "gb": default: if(city && postcode) city = city+" "+postcode; else if(postcode) city = postcode; break; } var ret = [ ]; if(name) ret.push(name); if(road) ret.push(road); if(suburb) ret.push(suburb); if(city) ret.push(city); if([ "residential", "town", "suburb", "village", "hamlet", "residential", "city", "county", "state" ].indexOf(type) != -1) { // Searching for a town if(county && county != city) ret.push(county); if(state && state != city) ret.push(state); } if(country) ret.push(country); return ret.join(", "); } /** * Checks whether the given query string is a representation of coordinates, such as * 48.123,5.123 or an OSM permalink. * @param query {String} * @return {Object} An object with the properties “lonlat” and “zoom” or null */ function isLonLatQuery(query) { var query = query.replace(/^\s+/, "").replace(/\s+$/, ""); var query_match,query_match2; if(query_match = query.match(/^http:\/\/(www\.)?osm\.org\/go\/([-A-Za-z0-9_@]+)/)) { // Coordinates, shortlink return decodeShortLink(query_match[2]); } if(query_match = query.match(/^(geo\s*:\s*)?(-?\s*\d+([.,]\d+)?)\s*[,;]\s*(-?\s*\d+([.,]\d+)?)(\s*\?z\s*=\s*(\d+))?$/)) { // Coordinates return { lat: 1*query_match[2].replace(",", ".").replace(/\s+/, ""), lon : 1*query_match[4].replace(",", ".").replace(/\s+/, ""), zoom : query_match[7] != null ? 1*query_match[7] : 15 }; } function decodeQueryString(str) { var lonMatch,latMatch,leafletMatch; if((lonMatch = str.match(/[?&]lat=([^&]+)/)) && (latMatch = str.match(/[?&]lat=([^&]+)/))) { return { lat: 1*decodeURIComponent(latMatch[1]), lon: 1*decodeURIComponent(lonMatch[1]), zoom: 15 }; } if(leafletMatch = str.match(/(^|=)(\d+)\/(-?\d+(\.\d+)?)\/(-?\d+(\.\d+)?)(&|\/|$)/)) { return { lat: leafletMatch[3], lon: leafletMatch[5], zoom: leafletMatch[2] }; } } if((query_match = query.match(/^https?:\/\/.*#(.*)$/)) && (query_match2 = decodeQueryString(query_match[1]))) { return query_match2; } if((query_match = query.match(/^https?:\/\/.*\?([^#]*)/)) && (query_match2 = decodeQueryString(query_match[1]))) { return query_match2; } return null; }; /** * Decodes a string from FacilMap.Util.encodeShortLink(). * @param encoded {String} * @return {Object} (lonlat: OpenLayers.LonLat, zoom: Number) */ function decodeShortLink(encoded) { var lon,lat,zoom; var m = encoded.match(/^([A-Za-z0-9_@]+)/); if(!m) return false; zoom = m[1].length*2+encoded.length-11; var c1 = 0; var c2 = 0; for(var i=0,j=54; i>> (30-j); else if(j > 30) c1 |= bits << (j-30); if(j < 30) c2 |= (bits & (0x3fffffff >>> j)) << j; } var x = 0; var y = 0; for(var j=29; j>0;) { x = (x << 1) | ((c1 >> j--) & 1); y = (y << 1) | ((c1 >> j--) & 1); } for(var j=29; j>0;) { x = (x << 1) | ((c2 >> j--) & 1); y = (y << 1) | ((c2 >> j--) & 1); } x *= 4; // We can’t do <<= 2 here as x and y may be greater than 2³¹ and then the value would become negative y *= 4; lon = x*90.0/(1<<30)-180.0; lat = y*45.0/(1<<30)-90.0; return { lat : Math.round(lat*100000)/100000, lon: Math.round(lon*100000)/100000, zoom : zoom }; } function loadUrl(url, completeOsmObjects) { return request(url, { encoding: null }).then(function(bodyBuf) { if(!bodyBuf) throw "Invalid response from server."; if(bodyBuf[0] == 0x42 && bodyBuf[1] == 0x5a && bodyBuf[2] == 0x68) {// bzip2 return new Buffer(compressjs.Bzip2.decompressFile(bodyBuf)); } else if(bodyBuf[0] == 0x1f && bodyBuf[1] == 0x8b && bodyBuf[2] == 0x08) // gzip return Promise.denodeify(zlib.gunzip.bind(zlib))(bodyBuf); else return bodyBuf; }).then(function(bodyBuf) { var body = bodyBuf.toString(); if(body.match(/^\s* 0) { return Promise.all(ret).then(function(relations) { relations.forEach(function(relation) { $.root().children().append(cheerio.load(relation, { xmlMode: true }).root().children().children()); }); return _loadSubRelations($); }); } else { return Promise.resolve(); } } module.exports = { find: find };