├── .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 |
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('