├── .gitignore ├── resources └── ajax-loader.gif ├── css └── citylimits.css ├── js ├── google.maps.Polygon.getBounds.js ├── utility-functions.js ├── relation-in-order.js └── city-boundaries-googlemaps.js ├── index.html ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.project -------------------------------------------------------------------------------- /resources/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgkelley4/city-boundaries-google-maps/HEAD/resources/ajax-loader.gif -------------------------------------------------------------------------------- /css/citylimits.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CityLimits example. 3 | * 4 | * Peter Kelley, pgkelley4@gmail.com 5 | */ 6 | 7 | html, body, #map-canvas { 8 | margin: 0; 9 | padding: 0; 10 | height: 100%; 11 | } 12 | #legend { 13 | background: #FFF; 14 | padding: 10px; 15 | margin: 5px; 16 | font-size: 12px; 17 | font-family: Arial, sans-serif; 18 | } 19 | .color { 20 | border: 1px solid; 21 | height: 12px; 22 | width: 12px; 23 | margin-right: 3px; 24 | float: left; 25 | } 26 | .colorFF0000 { 27 | background: #FF0000; 28 | } 29 | .color0000FF { 30 | background: #0000FF; 31 | } -------------------------------------------------------------------------------- /js/google.maps.Polygon.getBounds.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Polygon getBounds extension - google-maps-extensions 3 | * 4 | * @see http://code.google.com/p/google-maps-extensions/source/browse/google.maps.Polygon.getBounds.js 5 | */ 6 | if (!google.maps.Polygon.prototype.getBounds) { 7 | google.maps.Polygon.prototype.getBounds = function(latLng) { 8 | var bounds = new google.maps.LatLngBounds(); 9 | var paths = this.getPaths(); 10 | var path; 11 | 12 | for (var p = 0; p < paths.getLength(); p++) { 13 | path = paths.getAt(p); 14 | for (var i = 0; i < path.getLength(); i++) { 15 | bounds.extend(path.getAt(i)); 16 | } 17 | } 18 | 19 | return bounds; 20 | }; 21 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Load City Limits 15 | 16 | 17 | 18 | 21 | Ajax Loading 22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Peter G Kelley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /js/utility-functions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use the specified Object as a multi map. Insert 3 | * the value into an array for the key. If there 4 | * isn't an array for the key yet create one and 5 | * insert this value. 6 | * 7 | * @param {Object} object Object 8 | * @param {Object} key the key for the value 9 | * @param {Object} value a value corresponding to the key 10 | * 11 | */ 12 | function multiMapPut(object, key, value) { 13 | var valueArray = object[key]; 14 | if (valueArray == null) { 15 | valueArray = []; 16 | object[key] = valueArray; 17 | } 18 | valueArray.push(value); 19 | } 20 | 21 | /** 22 | * Request JSON from the specified URL. When it returns 23 | * call the callback function specified with the params 24 | * specified. 25 | * 26 | * @param {String} url URL to request JSON from 27 | * @param {function} callback function to call when this returns 28 | * @param {Array} array of optional parameters for the callback 29 | * the callback function specifies what is accepted here. 30 | */ 31 | function getRequestJSON(url, callback, params) { 32 | $.ajax({ 33 | type : 'GET', 34 | url : url, 35 | success : function(feed) { 36 | callback(feed, params); 37 | }, 38 | 39 | dataType : 'json' 40 | }); 41 | } 42 | 43 | /** 44 | * Set up jQuery to hide and show the loading element 45 | * when AJAX queries are running. 46 | */ 47 | function setUpAjax() { 48 | $.ajaxSetup({ 49 | beforeSend : function() { 50 | $("#loading").show(); 51 | }, 52 | complete : function() { 53 | $("#loading").hide(); 54 | } 55 | }); 56 | } 57 | 58 | 59 | /** 60 | * Convert the String to title case and return it. 61 | * 62 | * @param {String} str String to change to title case and return 63 | * 64 | * http://stackoverflow.com/a/196991/786339 65 | */ 66 | function toTitleCase(str) { 67 | return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); 68 | } -------------------------------------------------------------------------------- /js/relation-in-order.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Construct the paths from the relation specified JSON. The paths 3 | * will consist of nodes in the order that they connect through the 4 | * ways. 5 | * 6 | * Calls the callback specified with the following parameter: 7 | * 2-d array, an array of paths (arrays) of nodes and the parameters 8 | * passed in. 9 | * 10 | * Structure of OSM JSON and vaguely similar algorithm described 11 | * at the the following: 12 | * http://wiki.openstreetmap.org/wiki/Relation:multipolygon/Algorithm 13 | * 14 | * @param {JSON} relationJSON JSON response from OSM 15 | * @param {Array} params list of any parameters with the last 16 | * being the callback to execute when this is finished. The rest 17 | * of the parameters will be passed to the callback. 18 | */ 19 | function constructRelationInOrder(relationJSON, params) { 20 | var callback = params.pop(); 21 | 22 | /* 23 | * Read all the nodes, ways and relations found into memory in 24 | * structures convenient for retrieval later. 25 | */ 26 | var elements = relationJSON.elements; 27 | var nodes = {}; 28 | var ways = {}; 29 | var waysByStartNodeRef = {}; 30 | var waysByEndNodeRef = {}; 31 | for (i in elements) { 32 | if (elements[i].type == "way") { 33 | var way = elements[i]; 34 | ways[way.id] = way; 35 | multiMapPut(waysByStartNodeRef, way.nodes[0], way); 36 | multiMapPut(waysByEndNodeRef, way.nodes[way.nodes.length - 1], way); 37 | } else if (elements[i].type == "node") { 38 | nodes[elements[i].id] = elements[i]; 39 | } 40 | } 41 | 42 | /* 43 | * Add all nodes of all ways, when a way ends see if there is 44 | * the same node starting or ending another way, if so, add 45 | * all the nodes of that way forwards if the nodes starts the way 46 | * or backwards if the node ends the next way. 47 | */ 48 | var completedPaths = []; 49 | var nodesOfPath = []; 50 | // Was initially start at beginging of relation, but the ways aren't in order after this. 51 | // should post to openstreemap forums and see why this is 52 | // var startWay = relation.members[0]; 53 | var currentWayID; 54 | for (currentWayID in ways) break; 55 | var forwardTraversal = true; 56 | while (currentWayID != null) { 57 | var currentWay = ways[currentWayID]; 58 | delete ways[currentWayID]; 59 | 60 | if (forwardTraversal) { 61 | for (y in currentWay.nodes) { 62 | nodesOfPath.push(nodes[currentWay.nodes[y]]); 63 | } 64 | var endNode = currentWay.nodes[currentWay.nodes.length - 1]; 65 | } else { 66 | for (var y = currentWay.nodes.length - 1; y >= 0; y--) { 67 | nodesOfPath.push(nodes[currentWay.nodes[y]]); 68 | } 69 | var endNode = currentWay.nodes[0]; 70 | } 71 | 72 | var nextWayID = null; 73 | var wayArray = waysByStartNodeRef[endNode]; 74 | for (x in wayArray) { 75 | if (wayArray[x].id != currentWay.id && wayArray[x].id in ways) { 76 | nextWayID = wayArray[x].id; 77 | forwardTraversal = true; 78 | break; 79 | } 80 | } 81 | if (nextWayID == null) { 82 | wayArray = waysByEndNodeRef[endNode]; 83 | for (x in wayArray) { 84 | if (wayArray[x].id != currentWay.id && wayArray[x].id in ways) { 85 | nextWayID = wayArray[x].id; 86 | forwardTraversal = false; 87 | break; 88 | } 89 | } 90 | } 91 | 92 | // no connecting way found, must be complete 93 | if (nextWayID == null) { 94 | completedPaths.push(nodesOfPath); 95 | nodesOfPath = []; 96 | for (nextWayID in ways) break; 97 | forwardTraversal = true; 98 | } 99 | 100 | currentWayID = nextWayID; 101 | } 102 | 103 | callback(completedPaths, params); 104 | } 105 | 106 | /** 107 | * Get the relation as array of paths of nodes. Then call the 108 | * specified callback function with the array of paths as a 109 | * parameter. Therefore, callback specified must accept an array. 110 | * 111 | * @param {String} relationID ID of the relation to retrieve 112 | * @param {function} callback funciton, that is called when the 113 | * relation has been retrieved and processed. Must accept a 114 | * 2-d array, an array of paths (arrays) of nodes. 115 | * @param {Array} params an array of parameters to pass to the 116 | * specified callback. 117 | */ 118 | function getRelationInOrder(relationID, callback, params) { 119 | params.push(callback); 120 | getRequestJSON(getOSMCityRelationURL(relationID), constructRelationInOrder, params); 121 | } 122 | 123 | /** 124 | * Get the OpenStreetMap URL for a specific relation as a String. 125 | * 126 | * @param {String} relationID ID of the relation to retrieve 127 | */ 128 | function getOSMRelationURL(relationID) { 129 | return "http://overpass-api.de/api/interpreter?data=[out:json];(relation(" + relationID + ");>;);out;"; 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## City Boundaries With Google Maps 2 | 3 | This is a small example web app to illustrate adding city boundaries 4 | (limits) to Google Maps. The city data is read from the OpenStreetMap 5 | Overpass API: 6 | 7 | http://wiki.openstreetmap.org/wiki/Overpass_API 8 | 9 | ## Breakdown 10 | 11 | The JavaScript files used in this project are as follows: 12 | 13 | ### google.maps.Polygon.getBounds.js 14 | Polygon getBounds extension from google. Used to get the bounds of the 15 | final Polygon created for the cities boundaries and then zoom the map 16 | to that polygon. 17 | 18 | ### jquery-1.10.2.js 19 | Used for AJAX queries. 20 | 21 | ### city-boundaries-googlemaps.js 22 | The main JavaScript file that finds the city boundaries and displays 23 | them on a Google Maps canvas. Creates a legend with the city entered. 24 | 25 | ### utility-functions.js 26 | A few small utilities. 27 | 28 | ### relation-in-order.js 29 | This JavaScript gets the boundary relation specified and constructs an 30 | array of paths. These paths consist of the nodes that are latitude 31 | and longitude points on the boundary. 32 | 33 | Please see the Overpass background section for a discussion on relations, 34 | ways and nodes. 35 | 36 | To construct the paths this script does the following: 37 | ``` 38 | 1 grab any unprocessed way 39 | 2 add all nodes of way to new path 40 | 3 mark way as processed 41 | 4 find way that either starts or ends with the last node of the previous way 42 | 5 add all nodes of way forward or reverse depending on previous step 43 | 6 repeat steps 3 - 5 until no more connecting ways found, this is a complete path 44 | 7 repeat steps 1 - 6 until no more ways found at all 45 | ``` 46 | 47 | This must be done because neither the ways nor the relation are in order. 48 | 49 | This script does not do anything special for inner ways as Google Maps will 50 | handle discovering that a way is contained within another and display it 51 | properly. If not using Google Maps you will have to find if paths are 52 | contained within others. I wrote a script to do that here: 53 | 54 | https://github.com/pgkelley4/line-segments-intersect 55 | 56 | ## How to use 57 | 58 | You must include all the Javascript files in the js folder in your HTML page. 59 | To run this example, download the project and open the index.html file. 60 | 61 | ## Overpass background and Overpass QL used 62 | 63 | The OpenStreetMap Overpass API is a read-only API that provides OSM data. 64 | 65 | The relevant OpenStreetMap data is organized into relations, ways and nodes. 66 | 67 | * Relation - http://wiki.openstreetmap.org/wiki/Relation 68 | * Node - http://wiki.openstreetmap.org/wiki/Node 69 | * Way - http://wiki.openstreetmap.org/wiki/Way 70 | 71 | I find the relation, read its ways into memory and then add all their nodes 72 | into our paths in the correct order. 73 | 74 | The Overpass QL is used to query the Overpass API. Because the OSM data is 75 | incosistently labeled I have two sets of queries to find the correct area. 76 | These could certainly be tweaked/changed as I know they don't work all 77 | the time. See bugs section for more details. 78 | 79 | Query directly for the area: 80 | ``` 81 | area[name=%22" + cityName + "%22][%22is_in:state_code%22=%22" + stateName + "%22];foreach(out;); 82 | ``` 83 | 84 | If that doesn't work also query for associated nodes, and return each node's 85 | associated areas: 86 | ``` 87 | node[name=%22" + cityName + "%22][%22is_in%22~%22" + stateName + "%22];foreach(out;is_in;out;); 88 | ``` 89 | 90 | Then get the relation ID from the area ID by subtracting 3600000000 from it. 91 | 92 | To get the relation from its ID: 93 | ``` 94 | (relation(" + relationID + ");>;);out; 95 | ``` 96 | 97 | ## Notes 98 | 99 | This can handle cities that are made up of multiple polygons, for example 100 | New York, NY. It can also handle cities with holes in them, for example 101 | Detroit, MI. 102 | 103 | Sometimes the Overpass API goes much faster than other times. I use a busy 104 | indicator in the example to show when pinging the Overpass servers. 105 | 106 | ## Bugs 107 | 108 | The OpenStreetMap city boundary data is missing for some cities. Even some 109 | big ones like Los Angeles and Dallas. There isn't much I can do about this. 110 | 111 | Right now, this is more of a starting point, more work must be done to fix 112 | the queries to get the data for cities that are available but aren't found. 113 | An example is Houston, TX. 114 | 115 | There are still a few issues with drawing polygons with Google Maps as it 116 | seems to mess up occasionally. Currently, I am only aware of issues with 117 | San Antonio and I believe this is becuase it is messing up on some of the 118 | holes (or in OSM terminology, the inner ways of the boundary relation). -------------------------------------------------------------------------------- /js/city-boundaries-googlemaps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Small script to draw boundaries of a city on google maps. 3 | * 4 | * Retrieves city boundary data from openstreetmap 5 | * http://wiki.openstreetmap.org/wiki/Overpass_API 6 | * 7 | * @author Peter Kelley, August 12 2013 8 | */ 9 | 10 | var BOUNDARY_COLORS = ['FF0000']; 11 | var BOUNDARY_COLOR_COORDINATES_PARAM = 0; 12 | 13 | var map; 14 | var allOverlays = []; 15 | var lengendContent = []; 16 | 17 | google.maps.event.addDomListener(window, 'load', initialize); 18 | 19 | /** 20 | * Find the city boundaries and display on the google map. 21 | * 22 | */ 23 | function loadCityLimits() { 24 | // clear any previous polygons 25 | while (allOverlays[0]) { 26 | allOverlays.pop().setMap(null); 27 | } 28 | lengendContent = []; 29 | map.controls[google.maps.ControlPosition.RIGHT_TOP].clear(); 30 | 31 | var cityText = document.getElementById('cityTextInput'); 32 | var splitCity = cityText.value.split(","); 33 | if (splitCity.length != 2) { 34 | alert("Must enter a city in the format: CITY, STATE."); 35 | return; 36 | } 37 | 38 | var city = toTitleCase(splitCity[0].trim()); 39 | var state = splitCity[1].trim().toUpperCase(); 40 | 41 | var legendContents = []; 42 | 43 | var params = []; 44 | params[BOUNDARY_COLOR_COORDINATES_PARAM] = BOUNDARY_COLORS[0]; 45 | getRequestJSON(getOSMAreaForCityURL(city, state), processCityArea, params); 46 | 47 | var cityLegend = [city + ", " + state, params[BOUNDARY_COLOR_COORDINATES_PARAM]]; 48 | legendContents.push(cityLegend); 49 | 50 | addLegend(legendContents); 51 | } 52 | 53 | /** 54 | * Add a legend to the google map. This iterates through the legendContents 55 | * and creates legend entries for each elements of the array. 56 | * 57 | * @param {Array} legendContents array of the legend contents to to following 58 | * specifications: 59 | * legendContents[i][0] - Name of the legend entry 60 | * legendContents[i][1] - The color for the entry 61 | * 62 | * i specifies the entry number 63 | * 64 | */ 65 | function addLegend(legendContents) { 66 | // Create the legend and display on the map 67 | // https://developers.google.com/fusiontables/docs/samples/legend 68 | var legend = document.createElement('div'); 69 | legend.id = 'legend'; 70 | lengendContent.unshift('

Cities

'); 71 | for (x in legendContents) { 72 | lengendContent.push('

' + legendContents[x][0] + '

'); 73 | } 74 | legend.innerHTML = lengendContent.join(''); 75 | legend.index = 1; 76 | map.controls[google.maps.ControlPosition.RIGHT_TOP].push(legend); 77 | } 78 | 79 | /** 80 | * When the window has loaded, do basic initilization including 81 | * AJAX setup and Google maps set up including setting the 82 | * focus on the continental US 83 | */ 84 | function initialize() { 85 | $("#loading").hide(); 86 | setUpAjax(); 87 | 88 | var mapOptions = { 89 | zoom : 4, 90 | center : new google.maps.LatLng(37.09024, -95.712891), 91 | streetViewControl : false, 92 | mapTypeId : google.maps.MapTypeId.ROADMAP 93 | }; 94 | map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions); 95 | } 96 | 97 | /** 98 | * Get the OpenStreetMap URL for the area of a city. 99 | * 100 | * @param {String} cityName Name of the city to retrieve the area for 101 | * @param {String} stateName Name of the state to retrieve the area for 102 | */ 103 | function getOSMAreaForCityURL(cityName, stateName) { 104 | return "http://overpass-api.de/api/interpreter?data=[out:json];area[name=%22" + cityName + 105 | "%22][%22is_in:state_code%22=%22" + stateName + "%22];foreach(out;);node[name=%22" + cityName + 106 | "%22][%22is_in%22~%22" + stateName + "%22];foreach(out;is_in;out;);"; 107 | // case insensitive, really slow! 108 | // area[name~%22" + cityName + 109 | // "%22, i][%22is_in:state_code%22~%22" + stateName + "%22, i];foreach(out;);node[name~%22" + cityName + 110 | // "%22, i][%22is_in%22~%22" + stateName + "%22];foreach(out;is_in;out;); 111 | // could directly ping for relation 112 | //rel[name=Boston]["is_in:state_code"~MA];foreach(out;); 113 | } 114 | 115 | /** 116 | * Get the OpenStreetMap URL for a specific relation. 117 | * 118 | * @param {String} relationID ID of the relation to retrieve 119 | */ 120 | function getOSMCityRelationURL(relationID) { 121 | return "http://overpass-api.de/api/interpreter?data=[out:json];(relation(" + relationID + ");>;);out;"; 122 | } 123 | 124 | /** 125 | * Get the relation ID from the area JSON, request the relation and 126 | * construct the city boundaries from it. 127 | * 128 | * @param {JSON} areaJSON JSON area response from OSM 129 | * @param {Array} params list of any parameters to pass on to 130 | * city boundary callback constructMapFromBoundaries 131 | */ 132 | function processCityArea(areaJSON, params) { 133 | for (x in areaJSON.elements) { 134 | // if find something that is level 8 135 | // if find something labelled city 136 | // if find something that has the exact name 137 | if ((areaJSON.elements[x].tags.admin_level == "8" && 138 | areaJSON.elements[x].tags.border_type == null) || 139 | areaJSON.elements[x].tags.border_type == "city") { 140 | var areaID = areaJSON.elements[x].id; 141 | // transform to relation id, and get relation 142 | var relationID = areaID - 3600000000; 143 | 144 | getRelationInOrder(relationID, constructMapFromBoundaries, params); 145 | return; 146 | } 147 | } 148 | alert("Couldn't retrieve the city limits for a city, they are either missing from OpenStreetMap, not labeled " + 149 | "consistently or the city entered is not valid."); 150 | console.log("Failed to find city border from OSM."); 151 | } 152 | 153 | /** 154 | * Construct the polygons on the google map from the paths 155 | * and parameters specified. This is a callback that accepts 156 | * the parameters given to getRelationInOrder. 157 | * 158 | * @param {Array} paths Array of paths, which are an array of 159 | * OSM nodes. 160 | * @param {Array} params The parameters given to getRelationInOrder. 161 | * Of the format: 162 | * params[BOUNDARY_COLOR_COORDINATES_PARAM]; - Color to 163 | * make the polygon. 164 | */ 165 | function constructMapFromBoundaries(paths, params) { 166 | var color = params[BOUNDARY_COLOR_COORDINATES_PARAM]; 167 | 168 | for (i in paths) { 169 | var path = paths[i]; 170 | for (j in path) { 171 | var node = path[j]; 172 | path[j] = new google.maps.LatLng(node.lat, node.lon); 173 | } 174 | } 175 | 176 | // google maps api can create multiple polygons with one create call 177 | // and returns one object. Also can handle inner ways (holes) 178 | var polygon = createPolygon(paths, color); 179 | 180 | // set map zoom and location to new polygons 181 | map.fitBounds(polygon.getBounds()); 182 | } 183 | 184 | /** 185 | * Create a polygon on the google map of the specified 186 | * paths and color. 187 | * 188 | * @param {Array} paths Array of coordinates (google.maps.LatLng) 189 | * for this polygon 190 | * @param {String} The hex value for the color of the polygon, 191 | * omitting the # character 192 | */ 193 | function createPolygon(paths, color) { 194 | newPolygon = new google.maps.Polygon({ 195 | paths : paths, 196 | strokeColor : "#" + color, 197 | strokeOpacity : 0.8, 198 | strokeWeight : 2, 199 | fillColor : "#" + color, 200 | fillOpacity : 0.35, 201 | draggable : true 202 | // geodisc: true 203 | }); 204 | 205 | newPolygon.setMap(map); 206 | 207 | allOverlays.push(newPolygon); 208 | 209 | return newPolygon; 210 | } --------------------------------------------------------------------------------