├── README.md ├── css ├── app.css └── save_shape_to_gist.css ├── data ├── National_Parks.zip ├── Shaw_Historic_Sites.zip ├── bike_routes.geojson ├── leveed_area.geojson ├── parks.geojson ├── powercat.geojson ├── restaurants.geojson └── states_21basic.zip ├── exercise1_getting_set_up.md ├── exercise2_fork_me.md ├── exercise3_base_maps.md ├── exercise4_a_basic_map.md ├── exercise5_put_some_data_on_the_map.md ├── exercise6_adding_basic_popups.md ├── exercise7_fancy_click_handling.md ├── exercise8_bonus_locate_me.md ├── exercise9_super_bonus_add_directions.md ├── images ├── github.png └── load_folder.png ├── img ├── access_token.png ├── base_maps.png ├── classic.png ├── editor_projects.png ├── fork_me.png ├── home_page.png ├── new_project.png ├── profile.png └── project.png ├── index.html ├── js ├── exercise_4.js ├── exercise_5.js ├── exercise_6.js ├── exercise_7.js ├── exercise_8.js └── save_shape_to_gist.js ├── lib ├── decode.js ├── shp.js └── spin.js └── presentation └── Introduction to Web Mapping.pptx /README.md: -------------------------------------------------------------------------------- 1 | Web Mapping Workshop 2 | ==================== 3 | 4 | # What _is_ a web-map? 5 | 6 | There are a ton of different ways of putting mapping data on the web. Ranging from simple images to PDF's that the user has to download to dynamic and complex web mapping applications. 7 | 8 | > To simplify things a bit, when we say **web-map** we're talking about an interactive mapping application generally using a JavaScript framework to facilitate the basic pan, zoom and identify workflows native to interactive web mapping. 9 | 10 | We're also going to break the web-map into it's constituant parts in order to better understand how it is put together and how to implement our own. 11 | 12 | ## Data 13 | 14 | Data used on web-maps (and in any other kind of map for that matter) can be divided roughly into two groups, contextual and thematic. **Contextual** data generally provides the backdrop giving the map user context, both spatially and topically. **Thematic** data is the primary focus of the map, the data that is used to tell a story or get a point across. 15 | 16 | ### Contextual Data 17 | 18 | Base-maps are the most commonly used type of contextual data. Services such as Google Maps, MapQuest and MapBox provide base-maps in varying levels of cartographic customizability. These services pull their data (roads, cities, etc...) from large-scale commercial data aggregators such as TeleAtlas and Navteq, or from open data sources such as OpenStreetMap. 19 | 20 | Because of the relatively static nature of the base-map data, it is usually served up in image tile format. 21 | 22 | ###### Tiles you say? 23 | 24 | Pre-rendered maps of the base data are divided up into image tiles that are 256x256 px images. Image tiles are used to efficiently display base-map data. The most commonly used convention of tiling maps starts at zoom level 0 (z0). z0 is made up of a single 256x256px tile that covers the entire world. Each subsequent zoom level (down to 22 on average) is calculated by breaking up each tile from the previous zoom level into four equally sized tiles, rendered at 256x256px. 25 | 26 | For more information about tiles, check out [this MapBox article](https://www.mapbox.com/foundations/how-web-maps-work/) 27 | 28 | ### Thematic Data 29 | 30 | A successful map (web or otherwise) tells a story. This story is usually told using the thematic data that is displayed on top of the contextual base-map. Formats of thematic data can vary widely depending on the story to be told. Static datasets can be mapped showing a condition at a certain time, or temporal dimensions can be used to create animated maps showing changing conditions over time. 31 | 32 | Thematic data can be integrated into the tile base-map image service, as it's own tile based service, or as vector data that is drawn on top of the map in the browser. 33 | 34 | ###### A note about GeoJSON 35 | 36 | Data rendered on the map in the browser is commonly managed in a data format called GeoJSON. JSON (JavaScript Object Notation) is a way of organizing data piggy-backing on the way JavaScript handles objects. JSON is becoming ubiquitous on the web for transferring data from server to client and back. GeoJSON is a specification using JSON specifically for organizing geographic data. 37 | 38 | Check out the spec for [more information about GeoJSON](http://www.geojson.org) 39 | 40 | ## Mapping Client 41 | 42 | The mapping client is the glue that takes all of the data we talked about earlier and puts it on the web page in the right place, allowing you the user to pan around, zoom in and out and interrogate the mapped data. 43 | 44 | All of the widely used web-mapping client libraries are JavaScript based. These include: 45 | 46 | * Google Maps JavaScript API 47 | * ESRI ArcGIS JavaScript API 48 | * MapBox JavaScript API 49 | * Leaflet 50 | * OpenLayers 51 | * [many others...](http://techslides.com/50-javascript-libraries-and-plugins-for-maps/) 52 | 53 | ================= 54 | Ok, let's get started with [exercise 1, setting up our web map](https://github.com/willbreitkreutz/web_mapping_workshop/blob/gh-pages/exercise1_getting_set_up.md) 55 | -------------------------------------------------------------------------------- /css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | margin:0; 4 | padding:0; 5 | } 6 | p{ 7 | margin:0px; 8 | margin-bottom:5px; 9 | } 10 | 11 | #map { 12 | position:absolute; 13 | top:0; 14 | bottom:0; 15 | right:0; 16 | left:0; 17 | z-index:-99; 18 | } 19 | 20 | #sidebar{ 21 | position:absolute; top: 10px; right: 10px; bottom: 10px; width: 260px; 22 | background:#333; 23 | color: #fff; 24 | padding:20px; 25 | font-family: Arial, Helvetica, sans-serif; 26 | opacity:0.9; 27 | filter:alpha(opacity=80); /* For IE8 and earlier */ 28 | overflow: auto 29 | } 30 | 31 | ul { 32 | padding:0; 33 | margin:0; 34 | margin-top:10px; 35 | } 36 | 37 | li { 38 | list-style: none; 39 | padding-bottom:3px; 40 | padding-top:3px 41 | } 42 | 43 | li:hover { 44 | background-color:rgba(255,255,255,.2); 45 | } 46 | 47 | .post-transition { 48 | padding:0; 49 | margin:0; 50 | padding-left: 5px; 51 | font-style:italic; 52 | font-size: .8em; 53 | padding-bottom:5px; 54 | } 55 | 56 | .pre-transition { 57 | padding:0; 58 | margin:0; 59 | 60 | } 61 | 62 | a { 63 | text-decoration: none; 64 | color: #fff; 65 | } 66 | 67 | .styled-scrollbar::-webkit-scrollbar { 68 | width: 5px; 69 | height: 5px; 70 | } 71 | .styled-scrollbar::-webkit-scrollbar-track { 72 | background-color: rgba(255,255,255,.5); 73 | -webkit-border-radius: 5px; 74 | } 75 | .styled-scrollbar::-webkit-scrollbar-thumb:vertical { 76 | background-color: rgba(0,0,0,.2); 77 | -webkit-border-radius: 6px; 78 | } 79 | .styled-scrollbar::-webkit-scrollbar-thumb:vertical:hover, 80 | .styled-scrollbar::-webkit-scrollbar-thumb:horizontal:hover { 81 | background: #ccc; 82 | } 83 | .styled-scrollbar::-webkit-scrollbar-thumb:horizontal { 84 | background-color: rgba(255,255,255,.2); 85 | -webkit-border-radius: 6px; 86 | } 87 | -------------------------------------------------------------------------------- /css/save_shape_to_gist.css: -------------------------------------------------------------------------------- 1 | #drag-overlay { 2 | position:absolute; 3 | top:0; 4 | bottom:0; 5 | right:0; 6 | left:0; 7 | z-index:9999; 8 | background:#333; color: #fff; 9 | opacity:0.9; 10 | filter:alpha(opacity=80); /* For IE8 and earlier */ 11 | color:white; 12 | font: normal 700 32px sans-serif; 13 | } 14 | 15 | .drop-label { 16 | position:absolute; 17 | top:50%; 18 | left:50%; 19 | margin-right:-50%; 20 | transform:translate(-50%, -50%); 21 | 22 | } 23 | 24 | /* Query Control */ 25 | .nsi-control-query a{ 26 | background-image: url('../images/load_folder.png'); 27 | background-position: 5px 4px; 28 | background-repeat: no-repeat; 29 | background-size: 18px 18px; 30 | opacity: .8; 31 | } 32 | 33 | .nsi-control-query ul { 34 | display: none; 35 | left: 24px; 36 | list-style: none; 37 | margin: 0; 38 | padding: 0; 39 | position: absolute; 40 | top: 0; 41 | white-space: nowrap; 42 | z-index: -1; 43 | } 44 | .nsi-control-query ul li { 45 | display: inline-block; 46 | } 47 | .nsi-control-query ul li:first-child a { 48 | border-left: none; 49 | } 50 | .nsi-control-query ul li button { 51 | background-color: #919187; 52 | border: none; 53 | border-radius: 0; 54 | color: #FFF; 55 | display: block; 56 | font: 11px/19px "Helvetica Neue", Arial, Helvetica, sans-serif; 57 | line-height: 28px; 58 | text-align: center; 59 | text-decoration: none; 60 | padding-left: 10px; 61 | padding-right: 10px; 62 | height: 28px; 63 | width: auto; 64 | } 65 | .nsi-control-query ul li button:hover { 66 | background-color: #a0a098; 67 | } 68 | .nsi-control-query ul li button.pressed { 69 | background-color: #74746a; 70 | } 71 | .nsi-control-query ul li:last-child button { 72 | border-bottom-right-radius: 4px; 73 | border-top-right-radius: 4px; 74 | } 75 | 76 | .hide-input{ 77 | display:none!important; 78 | } 79 | 80 | /*query by shapefile control*/ 81 | /*Graph Control*/ 82 | .nsi-control-query-shape a.leaflet-bar-single { 83 | background-image: url('../images/github.png'); 84 | background-position: 4px 4px; 85 | background-repeat: no-repeat; 86 | background-size: 18px 18px; 87 | opacity: .8; 88 | } 89 | 90 | .nsi-control-query-shape ul { 91 | display: none; 92 | left: 24px; 93 | list-style: none; 94 | margin: 0; 95 | padding: 0; 96 | position: absolute; 97 | top: 0; 98 | white-space: nowrap; 99 | z-index: -1; 100 | } 101 | .nsi-control-query-shape ul li { 102 | display: inline-block; 103 | } 104 | .nsi-control-query-shape ul li:first-child a { 105 | border-left: none; 106 | } 107 | .nsi-control-query-shape ul li button { 108 | background-color: #919187; 109 | border: none; 110 | border-radius: 0; 111 | color: #FFF; 112 | display: block; 113 | font: 11px/19px "Helvetica Neue", Arial, Helvetica, sans-serif; 114 | line-height: 28px; 115 | text-align: center; 116 | text-decoration: none; 117 | padding-left: 10px; 118 | padding-right: 10px; 119 | height: 28px; 120 | width: auto; 121 | } 122 | .nsi-control-query-shape ul li button:hover { 123 | background-color: #a0a098; 124 | } 125 | .nsi-control-query-shape ul li button.pressed { 126 | background-color: #74746a; 127 | } 128 | .nsi-control-query-shape ul li:last-child button { 129 | border-bottom-right-radius: 4px; 130 | border-top-right-radius: 4px; 131 | } 132 | -------------------------------------------------------------------------------- /data/National_Parks.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/data/National_Parks.zip -------------------------------------------------------------------------------- /data/Shaw_Historic_Sites.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/data/Shaw_Historic_Sites.zip -------------------------------------------------------------------------------- /data/leveed_area.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "geometry": { 7 | "type": "Polygon", 8 | "coordinates": [ 9 | [ 10 | [ 11 | -82.09992900054111, 12 | 37.27841999988937 13 | ], 14 | [ 15 | -82.09979599947414, 16 | 37.2784490003212 17 | ], 18 | [ 19 | -82.09927799960437, 20 | 37.27845999987382 21 | ], 22 | [ 23 | -82.09865799980814, 24 | 37.278501999635026 25 | ], 26 | [ 27 | -82.09826400000878, 28 | 37.2783989999858 29 | ], 30 | [ 31 | -82.09728599960484, 32 | 37.2776439995101 33 | ], 34 | [ 35 | -82.09739600002722, 36 | 37.27746000038567 37 | ], 38 | [ 39 | -82.09757599995491, 40 | 37.27754199964294 41 | ], 42 | [ 43 | -82.09775699949694, 44 | 37.277593999850104 45 | ], 46 | [ 47 | -82.09778000025572, 48 | 37.27762100016613 49 | ], 50 | [ 51 | -82.09782599995742, 52 | 37.27763400015981 53 | ], 54 | [ 55 | -82.09791599970069, 56 | 37.27761599961864 57 | ], 58 | [ 59 | -82.09817899990473, 60 | 37.277641000086945 61 | ], 62 | [ 63 | -82.09821700046342, 64 | 37.27763900047094 65 | ], 66 | [ 67 | -82.09826400049099, 68 | 37.27760799991957 69 | ], 70 | [ 71 | -82.0984120000606, 72 | 37.27762400037829 73 | ], 74 | [ 75 | -82.09850399962029, 76 | 37.27761799963917 77 | ], 78 | [ 79 | -82.09869400058523, 80 | 37.277541999678384 81 | ], 82 | [ 83 | -82.09908500013945, 84 | 37.277502000044656 85 | ], 86 | [ 87 | -82.09949399944098, 88 | 37.27727199992529 89 | ], 90 | [ 91 | -82.09962699996139, 92 | 37.27712900010817 93 | ], 94 | [ 95 | -82.09966100001724, 96 | 37.2771209998713 97 | ], 98 | [ 99 | -82.0997470003703, 100 | 37.27704499966733 101 | ], 102 | [ 103 | -82.10009500003365, 104 | 37.27683399957616 105 | ], 106 | [ 107 | -82.10001699948529, 108 | 37.27704000036854 109 | ], 110 | [ 111 | -82.09996200006131, 112 | 37.27726700002699 113 | ], 114 | [ 115 | -82.09994000041634, 116 | 37.277658999634774 117 | ], 118 | [ 119 | -82.10000200054733, 120 | 37.278029999728766 121 | ], 122 | [ 123 | -82.10013600044535, 124 | 37.27837500018702 125 | ], 126 | [ 127 | -82.09992900054111, 128 | 37.27841999988937 129 | ] 130 | ] 131 | ] 132 | }, 133 | "properties": { 134 | "OBJECTID_1": 1, 135 | "OBJECTID": 423, 136 | "LEVEED_ID": "3306000029", 137 | "FC_SYSTEM_": "3305000029", 138 | "SYSTEM_NAM": "Grundy, VA, LPP", 139 | "LEVEE_STAT": "", 140 | "FEATURE_NA": "Grundy, VA, LPP", 141 | "WARNING_SY": "", 142 | "LEVEED_ARE": "Top of Levee", 143 | "MINIMUM_OV": "", 144 | "EGRESS_LOC": "2", 145 | "EVACUATION": "", 146 | "COMPUTED_S": "", 147 | "COMPUTED_1": "", 148 | "FLOOD_RECO": "", 149 | "FLOOD_RE_1": "", 150 | "SHAPE_Leng": 0.00793596028, 151 | "Shape_Le_1": 764.413917839, 152 | "Shape_Area": 22668.5200045 153 | } 154 | } 155 | ] 156 | } 157 | -------------------------------------------------------------------------------- /data/powercat.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": { 7 | "Id": 0, 8 | "title": "", 9 | "description": "" 10 | }, 11 | "geometry": { 12 | "type": "Polygon", 13 | "coordinates": [ 14 | [ 15 | [ 16 | -96.661955, 17 | 39.252598 18 | ], 19 | [ 20 | -96.593434, 21 | 39.248399 22 | ], 23 | [ 24 | -96.593133, 25 | 39.245484 26 | ], 27 | [ 28 | -96.593133, 29 | 39.242568 30 | ], 31 | [ 32 | -96.594488, 33 | 39.239536 34 | ], 35 | [ 36 | -96.596145, 37 | 39.236736 38 | ], 39 | [ 40 | -96.599157, 41 | 39.23452 42 | ], 43 | [ 44 | -96.602621, 45 | 39.232537 46 | ], 47 | [ 48 | -96.607289, 49 | 39.230671 50 | ], 51 | [ 52 | -96.612108, 53 | 39.229621 54 | ], 55 | [ 56 | -96.617529, 57 | 39.229271 58 | ], 59 | [ 60 | -96.621445, 61 | 39.229154 62 | ], 63 | [ 64 | -96.626565, 65 | 39.229971 66 | ], 67 | [ 68 | -96.632288, 69 | 39.231254 70 | ], 71 | [ 72 | -96.636655, 73 | 39.23277 74 | ], 75 | [ 76 | -96.642076, 77 | 39.235453 78 | ], 79 | [ 80 | -96.647799, 81 | 39.238719 82 | ], 83 | [ 84 | -96.652467, 85 | 39.242335 86 | ], 87 | [ 88 | -96.657587, 89 | 39.247233 90 | ], 91 | [ 92 | -96.661955, 93 | 39.252598 94 | ] 95 | ] 96 | ] 97 | }, 98 | "id": "ci25vrn8c4c6ia7pcuhlehvb4" 99 | }, 100 | { 101 | "type": "Feature", 102 | "properties": { 103 | "Id": 0, 104 | "title": "", 105 | "description": "" 106 | }, 107 | "geometry": { 108 | "type": "Polygon", 109 | "coordinates": [ 110 | [ 111 | [ 112 | -96.698248, 113 | 39.242685 114 | ], 115 | [ 116 | -96.66587, 117 | 39.247816 118 | ], 119 | [ 120 | -96.663009, 121 | 39.244901 122 | ], 123 | [ 124 | -96.658943, 125 | 39.241402 126 | ], 127 | [ 128 | -96.655328, 129 | 39.238603 130 | ], 131 | [ 132 | -96.65066, 133 | 39.235337 134 | ], 135 | [ 136 | -96.646293, 137 | 39.23277 138 | ], 139 | [ 140 | -96.641624, 141 | 39.230671 142 | ], 143 | [ 144 | -96.637408, 145 | 39.228454 146 | ], 147 | [ 148 | -96.631986, 149 | 39.226588 150 | ], 151 | [ 152 | -96.626565, 153 | 39.225538 154 | ], 155 | [ 156 | -96.620993, 157 | 39.224838 158 | ], 159 | [ 160 | -96.614668, 161 | 39.224838 162 | ], 163 | [ 164 | -96.608343, 165 | 39.225538 166 | ], 167 | [ 168 | -96.602771, 169 | 39.227171 170 | ], 171 | [ 172 | -96.598705, 173 | 39.229388 174 | ], 175 | [ 176 | -96.594037, 177 | 39.23242 178 | ], 179 | [ 180 | -96.591326, 181 | 39.23557 182 | ], 183 | [ 184 | -96.589218, 185 | 39.239186 186 | ], 187 | [ 188 | -96.587712, 189 | 39.243151 190 | ], 191 | [ 192 | -96.586959, 193 | 39.247233 194 | ], 195 | [ 196 | -96.587109, 197 | 39.248983 198 | ], 199 | [ 200 | -96.580935, 201 | 39.248749 202 | ], 203 | [ 204 | -96.57461, 205 | 39.248399 206 | ], 207 | [ 208 | -96.567984, 209 | 39.247933 210 | ], 211 | [ 212 | -96.559852, 213 | 39.24735 214 | ], 215 | [ 216 | -96.552473, 217 | 39.246533 218 | ], 219 | [ 220 | -96.54419, 221 | 39.2456 222 | ], 223 | [ 224 | -96.535606, 225 | 39.244318 226 | ], 227 | [ 228 | -96.528378, 229 | 39.243151 230 | ], 231 | [ 232 | -96.520396, 233 | 39.241402 234 | ], 235 | [ 236 | -96.512716, 237 | 39.239652 238 | ], 239 | [ 240 | -96.504885, 241 | 39.23732 242 | ], 243 | [ 244 | -96.498108, 245 | 39.23452 246 | ], 247 | [ 248 | -96.492687, 249 | 39.231954 250 | ], 251 | [ 252 | -96.492687, 253 | 39.230904 254 | ], 255 | [ 256 | -96.493139, 257 | 39.230671 258 | ], 259 | [ 260 | -96.511662, 261 | 39.231721 262 | ], 263 | [ 264 | -96.507295, 265 | 39.230904 266 | ], 267 | [ 268 | -96.496301, 269 | 39.228688 270 | ], 271 | [ 272 | -96.487717, 273 | 39.226471 274 | ], 275 | [ 276 | -96.479435, 277 | 39.224138 278 | ], 279 | [ 280 | -96.472809, 281 | 39.222271 282 | ], 283 | [ 284 | -96.465881, 285 | 39.220055 286 | ], 287 | [ 288 | -96.457448, 289 | 39.216788 290 | ], 291 | [ 292 | -96.450521, 293 | 39.214104 294 | ], 295 | [ 296 | -96.443443, 297 | 39.210837 298 | ], 299 | [ 300 | -96.439226, 301 | 39.208504 302 | ], 303 | [ 304 | -96.433654, 305 | 39.205236 306 | ], 307 | [ 308 | -96.429739, 309 | 39.202552 310 | ], 311 | [ 312 | -96.427179, 313 | 39.200452 314 | ], 315 | [ 316 | -96.427028, 317 | 39.198118 318 | ], 319 | [ 320 | -96.430944, 321 | 39.192399 322 | ], 323 | [ 324 | -96.436365, 325 | 39.184579 326 | ], 327 | [ 328 | -96.441334, 329 | 39.178859 330 | ], 331 | [ 332 | -96.446455, 333 | 39.173255 334 | ], 335 | [ 336 | -96.451274, 337 | 39.169052 338 | ], 339 | [ 340 | -96.460912, 341 | 39.169869 342 | ], 343 | [ 344 | -96.464074, 345 | 39.165783 346 | ], 347 | [ 348 | -96.466785, 349 | 39.163097 350 | ], 351 | [ 352 | -96.470098, 353 | 39.160762 354 | ], 355 | [ 356 | -96.472056, 357 | 39.159828 358 | ], 359 | [ 360 | -96.475068, 361 | 39.159945 362 | ], 363 | [ 364 | -96.47326, 365 | 39.162046 366 | ], 367 | [ 368 | -96.471754, 369 | 39.165082 370 | ], 371 | [ 372 | -96.471604, 373 | 39.168585 374 | ], 375 | [ 376 | -96.472658, 377 | 39.172788 378 | ], 379 | [ 380 | -96.475971, 381 | 39.177341 382 | ], 383 | [ 384 | -96.482145, 385 | 39.180726 386 | ], 387 | [ 388 | -96.487567, 389 | 39.182361 390 | ], 391 | [ 392 | -96.493591, 393 | 39.182361 394 | ], 395 | [ 396 | -96.49856, 397 | 39.181894 398 | ], 399 | [ 400 | -96.505487, 401 | 39.179793 402 | ], 403 | [ 404 | -96.513017, 405 | 39.176407 406 | ], 407 | [ 408 | -96.516933, 409 | 39.173489 410 | ], 411 | [ 412 | -96.520547, 413 | 39.17057 414 | ], 415 | [ 416 | -96.522655, 417 | 39.17057 418 | ], 419 | [ 420 | -96.52642, 421 | 39.172438 422 | ], 423 | [ 424 | -96.532594, 425 | 39.174773 426 | ], 427 | [ 428 | -96.538618, 429 | 39.176407 430 | ], 431 | [ 432 | -96.544761, 433 | 39.178513 434 | ], 435 | [ 436 | -96.553226, 437 | 39.179676 438 | ], 439 | [ 440 | -96.561659, 441 | 39.181077 442 | ], 443 | [ 444 | -96.569791, 445 | 39.182244 446 | ], 447 | [ 448 | -96.57717, 449 | 39.183061 450 | ], 451 | [ 452 | -96.586658, 453 | 39.183761 454 | ], 455 | [ 456 | -96.596898, 457 | 39.183761 458 | ], 459 | [ 460 | -96.608945, 461 | 39.183178 462 | ], 463 | [ 464 | -96.619336, 465 | 39.182361 466 | ], 467 | [ 468 | -96.627318, 469 | 39.181077 470 | ], 471 | [ 472 | -96.637709, 473 | 39.179442 474 | ], 475 | [ 476 | -96.64569, 477 | 39.177691 478 | ], 479 | [ 480 | -96.657286, 481 | 39.174889 482 | ], 483 | [ 484 | -96.664515, 485 | 39.172788 486 | ], 487 | [ 488 | -96.672647, 489 | 39.169753 490 | ], 491 | [ 492 | -96.679574, 493 | 39.166717 494 | ], 495 | [ 496 | -96.684995, 497 | 39.164031 498 | ], 499 | [ 500 | -96.691019, 501 | 39.160529 502 | ], 503 | [ 504 | -96.695989, 505 | 39.157142 506 | ], 507 | [ 508 | -96.700808, 509 | 39.152705 510 | ], 511 | [ 512 | -96.705476, 513 | 39.14815 514 | ], 515 | [ 516 | -96.707886, 517 | 39.145698 518 | ], 519 | [ 520 | -96.698248, 521 | 39.242685 522 | ] 523 | ] 524 | ] 525 | }, 526 | "id": "ci25vrn8e4c6ja7pc2134p1ep" 527 | }, 528 | { 529 | "type": "Feature", 530 | "properties": { 531 | "Id": 0, 532 | "title": "", 533 | "description": "" 534 | }, 535 | "geometry": { 536 | "type": "Polygon", 537 | "coordinates": [ 538 | [ 539 | [ 540 | -96.524462, 541 | 39.167534 542 | ], 543 | [ 544 | -96.526269, 545 | 39.163681 546 | ], 547 | [ 548 | -96.527775, 549 | 39.159127 550 | ], 551 | [ 552 | -96.535907, 553 | 39.160529 554 | ], 555 | [ 556 | -96.545696, 557 | 39.161813 558 | ], 559 | [ 560 | -96.556238, 561 | 39.16263 562 | ], 563 | [ 564 | -96.56452, 565 | 39.16263 566 | ], 567 | [ 568 | -96.573556, 569 | 39.162163 570 | ], 571 | [ 572 | -96.581688, 573 | 39.161229 574 | ], 575 | [ 576 | -96.589519, 577 | 39.159478 578 | ], 579 | [ 580 | -96.595844, 581 | 39.157259 582 | ], 583 | [ 584 | -96.602169, 585 | 39.154106 586 | ], 587 | [ 588 | -96.608343, 589 | 39.150135 590 | ], 591 | [ 592 | -96.613162, 593 | 39.145464 594 | ], 595 | [ 596 | -96.616927, 597 | 39.140442 598 | ], 599 | [ 600 | -96.620089, 601 | 39.134017 602 | ], 603 | [ 604 | -96.621897, 605 | 39.129228 606 | ], 607 | [ 608 | -96.6228, 609 | 39.124088 610 | ], 611 | [ 612 | -96.622047, 613 | 39.11848 614 | ], 615 | [ 616 | -96.620993, 617 | 39.114273 618 | ], 619 | [ 620 | -96.618734, 621 | 39.109483 622 | ], 623 | [ 624 | -96.615572, 625 | 39.104224 626 | ], 627 | [ 628 | -96.61271, 629 | 39.100368 630 | ], 631 | [ 632 | -96.609698, 633 | 39.097446 634 | ], 635 | [ 636 | -96.613915, 637 | 39.096277 638 | ], 639 | [ 640 | -96.621746, 641 | 39.095225 642 | ], 643 | [ 644 | -96.628673, 645 | 39.095108 646 | ], 647 | [ 648 | -96.636655, 649 | 39.095576 650 | ], 651 | [ 652 | -96.643281, 653 | 39.096628 654 | ], 655 | [ 656 | -96.649907, 657 | 39.098498 658 | ], 659 | [ 660 | -96.65563, 661 | 39.100835 662 | ], 663 | [ 664 | -96.661352, 665 | 39.103523 666 | ], 667 | [ 668 | -96.667075, 669 | 39.107262 670 | ], 671 | [ 672 | -96.672195, 673 | 39.111352 674 | ], 675 | [ 676 | -96.676713, 677 | 39.11661 678 | ], 679 | [ 680 | -96.680026, 681 | 39.122686 682 | ], 683 | [ 684 | -96.682586, 685 | 39.129111 686 | ], 687 | [ 688 | -96.682736, 689 | 39.134134 690 | ], 691 | [ 692 | -96.682285, 693 | 39.139157 694 | ], 695 | [ 696 | -96.680779, 697 | 39.144179 698 | ], 699 | [ 700 | -96.678369, 701 | 39.148851 702 | ], 703 | [ 704 | -96.674454, 705 | 39.154106 706 | ], 707 | [ 708 | -96.669785, 709 | 39.158543 710 | ], 711 | [ 712 | -96.664515, 713 | 39.162163 714 | ], 715 | [ 716 | -96.659545, 717 | 39.164966 718 | ], 719 | [ 720 | -96.653672, 721 | 39.167651 722 | ], 723 | [ 724 | -96.647196, 725 | 39.17022 726 | ], 727 | [ 728 | -96.639667, 729 | 39.172671 730 | ], 731 | [ 732 | -96.631986, 733 | 39.174422 734 | ], 735 | [ 736 | -96.622047, 737 | 39.176057 738 | ], 739 | [ 740 | -96.612409, 741 | 39.177224 742 | ], 743 | [ 744 | -96.601265, 745 | 39.178041 746 | ], 747 | [ 748 | -96.58982, 749 | 39.178275 750 | ], 751 | [ 752 | -96.575815, 753 | 39.177341 754 | ], 755 | [ 756 | -96.561659, 757 | 39.17559 758 | ], 759 | [ 760 | -96.54916, 761 | 39.173255 762 | ], 763 | [ 764 | -96.536058, 765 | 39.17057 766 | ], 767 | [ 768 | -96.524462, 769 | 39.167534 770 | ] 771 | ] 772 | ] 773 | }, 774 | "id": "ci25vrn8f4c6ka7pcyi2wqqn2" 775 | }, 776 | { 777 | "type": "Feature", 778 | "properties": { 779 | "Id": 0, 780 | "title": "", 781 | "description": "" 782 | }, 783 | "geometry": { 784 | "type": "Polygon", 785 | "coordinates": [ 786 | [ 787 | [ 788 | -96.484254, 789 | 39.150369 790 | ], 791 | [ 792 | -96.482898, 793 | 39.149668 794 | ], 795 | [ 796 | -96.483049, 797 | 39.147333 798 | ], 799 | [ 800 | -96.485007, 801 | 39.142427 802 | ], 803 | [ 804 | -96.48847, 805 | 39.136587 806 | ], 807 | [ 808 | -96.482145, 809 | 39.130863 810 | ], 811 | [ 812 | -96.487115, 813 | 39.126074 814 | ], 815 | [ 816 | -96.49344, 817 | 39.121167 818 | ], 819 | [ 820 | -96.500367, 821 | 39.116961 822 | ], 823 | [ 824 | -96.508048, 825 | 39.113105 826 | ], 827 | [ 828 | -96.514824, 829 | 39.110067 830 | ], 831 | [ 832 | -96.522806, 833 | 39.107496 834 | ], 835 | [ 836 | -96.531088, 837 | 39.104925 838 | ], 839 | [ 840 | -96.531992, 841 | 39.10773 842 | ], 843 | [ 844 | -96.535456, 845 | 39.111586 846 | ], 847 | [ 848 | -96.538769, 849 | 39.114157 850 | ], 851 | [ 852 | -96.543738, 853 | 39.116844 854 | ], 855 | [ 856 | -96.549913, 857 | 39.118129 858 | ], 859 | [ 860 | -96.555635, 861 | 39.119064 862 | ], 863 | [ 864 | -96.560605, 865 | 39.118713 866 | ], 867 | [ 868 | -96.565876, 869 | 39.117662 870 | ], 871 | [ 872 | -96.570695, 873 | 39.115909 874 | ], 875 | [ 876 | -96.575062, 877 | 39.112988 878 | ], 879 | [ 880 | -96.579128, 881 | 39.109483 882 | ], 883 | [ 884 | -96.581387, 885 | 39.107029 886 | ], 887 | [ 888 | -96.585754, 889 | 39.110184 890 | ], 891 | [ 892 | -96.590272, 893 | 39.114858 894 | ], 895 | [ 896 | -96.592983, 897 | 39.119765 898 | ], 899 | [ 900 | -96.594639, 901 | 39.124204 902 | ], 903 | [ 904 | -96.59494, 905 | 39.12806 906 | ], 907 | [ 908 | -96.594187, 909 | 39.132032 910 | ], 911 | [ 912 | -96.592681, 913 | 39.136237 914 | ], 915 | [ 916 | -96.590121, 917 | 39.140675 918 | ], 919 | [ 920 | -96.585905, 921 | 39.145347 922 | ], 923 | [ 924 | -96.580182, 925 | 39.149435 926 | ], 927 | [ 928 | -96.574008, 929 | 39.152121 930 | ], 931 | [ 932 | -96.566478, 933 | 39.154223 934 | ], 935 | [ 936 | -96.560454, 937 | 39.155274 938 | ], 939 | [ 940 | -96.555183, 941 | 39.155858 942 | ], 943 | [ 944 | -96.547804, 945 | 39.156091 946 | ], 947 | [ 948 | -96.539221, 949 | 39.155624 950 | ], 951 | [ 952 | -96.530185, 953 | 39.154456 954 | ], 955 | [ 956 | -96.52883, 957 | 39.153289 958 | ], 959 | [ 960 | -96.527474, 961 | 39.149084 962 | ], 963 | [ 964 | -96.525366, 965 | 39.145814 966 | ], 967 | [ 968 | -96.522806, 969 | 39.143478 970 | ], 971 | [ 972 | -96.519192, 973 | 39.141026 974 | ], 975 | [ 976 | -96.514674, 977 | 39.139624 978 | ], 979 | [ 980 | -96.508499, 981 | 39.138923 982 | ], 983 | [ 984 | -96.502777, 985 | 39.139274 986 | ], 987 | [ 988 | -96.496151, 989 | 39.141726 990 | ], 991 | [ 992 | -96.490428, 993 | 39.14523 994 | ], 995 | [ 996 | -96.487416, 997 | 39.147449 998 | ], 999 | [ 1000 | -96.484254, 1001 | 39.150369 1002 | ] 1003 | ] 1004 | ] 1005 | }, 1006 | "id": "ci25vrn8h4c6la7pcl60u2hf3" 1007 | } 1008 | ] 1009 | } 1010 | -------------------------------------------------------------------------------- /data/restaurants.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 4 | 5 | "features": [ 6 | { "type": "Feature", "properties": { "@id": "node\/821918814", "name": "Shaba-Shabu", "phone": null, "website": null, "cuisine": "asian" }, "geometry": { "type": "Point", "coordinates": [ -78.6210263, 35.8237678 ] } }, 7 | { "type": "Feature", "properties": { "@id": "node\/821918815", "name": "Melting Pot", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.621377, 35.824287 ] } }, 8 | { "type": "Feature", "properties": { "@id": "node\/935419273", "name": "Jasmin Mediterranean Bistro", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.667869, 35.787874 ] } }, 9 | { "type": "Feature", "properties": { "@id": "node\/1055457666", "name": "Big Ed's City Market Restaurant", "phone": "(919) 836-9909", "website": "http:\/\/www.bigedscitymarket.com\/", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6359932, 35.7760831 ] } }, 10 | { "type": "Feature", "properties": { "@id": "node\/1055457679", "name": "Battistella's", "phone": "(919) 803-2501", "website": "http:\/\/battistellas.com\/", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6365233, 35.7767331 ] } }, 11 | { "type": "Feature", "properties": { "@id": "node\/1055457691", "name": "Woody's at City Market", "phone": null, "website": "http:\/\/woodyscitymarket.com\/", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6364362, 35.7763657 ] } }, 12 | { "type": "Feature", "properties": { "@id": "node\/1216186193", "name": "Solas", "phone": "+1 919 7550755", "website": "http:\/\/solasraleigh.com\/", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6475829, 35.7856609 ] } }, 13 | { "type": "Feature", "properties": { "@id": "node\/1216186217", "name": "Sushi O Bistro", "phone": null, "website": null, "cuisine": "sushi" }, "geometry": { "type": "Point", "coordinates": [ -78.6472448, 35.7836279 ] } }, 14 | { "type": "Feature", "properties": { "@id": "node\/1216186227", "name": "Sullivan's Steakhouse", "phone": null, "website": null, "cuisine": "steak_house" }, "geometry": { "type": "Point", "coordinates": [ -78.6471269, 35.786009 ] } }, 15 | { "type": "Feature", "properties": { "@id": "node\/1216186232", "name": "Tobbaco Road Sports Cafe", "phone": null, "website": null, "cuisine": "american" }, "geometry": { "type": "Point", "coordinates": [ -78.6472426, 35.7837673 ] } }, 16 | { "type": "Feature", "properties": { "@id": "node\/1216186268", "name": "Brooklyn Heights", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6474541, 35.7876496 ] } }, 17 | { "type": "Feature", "properties": { "@id": "node\/1216186366", "name": "Rockford", "phone": "+1 919 8219020", "website": "http:\/\/www.therockfordrestaurant.com\/", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6472066, 35.7847558 ] } }, 18 | { "type": "Feature", "properties": { "@id": "node\/1216186384", "name": "Ciago's", "phone": null, "website": null, "cuisine": "italian" }, "geometry": { "type": "Point", "coordinates": [ -78.650629, 35.7872536 ] } }, 19 | { "type": "Feature", "properties": { "@id": "node\/1216186394", "name": "Thaiphoon", "phone": "+1 919 7204034", "website": "http:\/\/www.thaiphoonbistro.com\/", "cuisine": "thai" }, "geometry": { "type": "Point", "coordinates": [ -78.6476259, 35.7845763 ] } }, 20 | { "type": "Feature", "properties": { "@id": "node\/1216186407", "name": "Sushi Blues Cafe", "phone": "+1 919 6648061", "website": "http:\/\/sushibluescafe.com\/", "cuisine": "japanese" }, "geometry": { "type": "Point", "coordinates": [ -78.6476243, 35.7844382 ] } }, 21 | { "type": "Feature", "properties": { "@id": "node\/1216186410", "name": "Sahara Mediterranean", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6504037, 35.7884113 ] } }, 22 | { "type": "Feature", "properties": { "@id": "node\/1216186416", "name": "Armadillo Grill", "phone": "+1 919 5460555", "website": "http:\/\/armadillogrill.com\/", "cuisine": "mexican" }, "geometry": { "type": "Point", "coordinates": [ -78.647497, 35.7861004 ] } }, 23 | { "type": "Feature", "properties": { "@id": "node\/1216186442", "name": "Pho Pho Pho", "phone": null, "website": null, "cuisine": "asian" }, "geometry": { "type": "Point", "coordinates": [ -78.6470947, 35.7869622 ] } }, 24 | { "type": "Feature", "properties": { "@id": "node\/1216186460", "name": "Tasca Brava", "phone": null, "website": null, "cuisine": "spanish" }, "geometry": { "type": "Point", "coordinates": [ -78.6474541, 35.7878021 ] } }, 25 | { "type": "Feature", "properties": { "@id": "node\/1216186476", "name": "MoJoe's Burger Joint", "phone": null, "website": null, "cuisine": "burger" }, "geometry": { "type": "Point", "coordinates": [ -78.6469767, 35.7885286 ] } }, 26 | { "type": "Feature", "properties": { "@id": "node\/1278590464", "name": "Fresh@Five Points", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6464431, 35.8047438 ] } }, 27 | { "type": "Feature", "properties": { "@id": "node\/1411329838", "name": "El Cerro", "phone": null, "website": null, "cuisine": "mexican" }, "geometry": { "type": "Point", "coordinates": [ -78.6767082, 35.7786023 ] } }, 28 | { "type": "Feature", "properties": { "@id": "node\/1411329841", "name": "The Village Deli", "phone": null, "website": null, "cuisine": "sandwich" }, "geometry": { "type": "Point", "coordinates": [ -78.6607677, 35.7912635 ] } }, 29 | { "type": "Feature", "properties": { "@id": "node\/1411329847", "name": "Moe's Southwest Grill", "phone": null, "website": null, "cuisine": "mexican" }, "geometry": { "type": "Point", "coordinates": [ -78.6607706, 35.7914964 ] } }, 30 | { "type": "Feature", "properties": { "@id": "node\/1411329848", "name": "Baja Burrito", "phone": null, "website": null, "cuisine": "mexican" }, "geometry": { "type": "Point", "coordinates": [ -78.6755239, 35.7795584 ] } }, 31 | { "type": "Feature", "properties": { "@id": "node\/1908757864", "name": "Dalat Oriental cuisine", "phone": null, "website": null, "cuisine": "vietnamese" }, "geometry": { "type": "Point", "coordinates": [ -78.6754514, 35.7795345 ] } }, 32 | { "type": "Feature", "properties": { "@id": "node\/1908829780", "name": "Abyssinia", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6753737, 35.779368 ] } }, 33 | { "type": "Feature", "properties": { "@id": "node\/2072382353", "name": "Flying Saucer Draught Emporium", "phone": null, "website": null, "cuisine": "american" }, "geometry": { "type": "Point", "coordinates": [ -78.6447955, 35.7799487 ] } }, 34 | { "type": "Feature", "properties": { "@id": "node\/2072382489", "name": "The Roast Grill", "phone": null, "website": null, "cuisine": "american" }, "geometry": { "type": "Point", "coordinates": [ -78.6457571, 35.780248 ] } }, 35 | { "type": "Feature", "properties": { "@id": "node\/2072383501", "name": "Second Empire Restaurant and Tavern", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6446923, 35.7808964 ] } }, 36 | { "type": "Feature", "properties": { "@id": "node\/2098187833", "name": "Babylon Restaurant", "phone": null, "website": null, "cuisine": "Moroccan" }, "geometry": { "type": "Point", "coordinates": [ -78.6435513, 35.7845575 ] } }, 37 | { "type": "Feature", "properties": { "@id": "node\/2144920174", "name": "The Pit", "phone": "+1 919 890 4500", "website": "www.thepit-bbq.com", "cuisine": "barbecue" }, "geometry": { "type": "Point", "coordinates": [ -78.6447083, 35.7759121 ] } }, 38 | { "type": "Feature", "properties": { "@id": "node\/2147107893", "name": "Sitti Authentic Lebanese", "phone": null, "website": null, "cuisine": "lebanese" }, "geometry": { "type": "Point", "coordinates": [ -78.6380771, 35.7783596 ] } }, 39 | { "type": "Feature", "properties": { "@id": "node\/2198231992", "name": "Mecca Restaurant", "phone": "+1-919-832-5714", "website": "http:\/\/www.mecca-restaurant.com\/", "cuisine": "american" }, "geometry": { "type": "Point", "coordinates": [ -78.6386608, 35.7770398 ] } }, 40 | { "type": "Feature", "properties": { "@id": "node\/2566339186", "name": "Bida Manda", "phone": null, "website": null, "cuisine": "Laotion,_Thai" }, "geometry": { "type": "Point", "coordinates": [ -78.6367789, 35.7772541 ] } }, 41 | { "type": "Feature", "properties": { "@id": "node\/2566377007", "name": "El Rodeo Mexican Restaurant", "phone": "(919) 829-0777", "website": "http:\/\/elrodeonc.com\/downtown-raleigh\/locations\/", "cuisine": "mexican" }, "geometry": { "type": "Point", "coordinates": [ -78.6365704, 35.7756429 ] } }, 42 | { "type": "Feature", "properties": { "@id": "node\/2566388184", "name": "42nd Street Oyster Bar", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.64624, 35.7831205 ] } }, 43 | { "type": "Feature", "properties": { "@id": "node\/2566771653", "name": "Bolt Bistro & Bar", "phone": "+1-919-821-0011", "website": "http:\/\/www.boltbistro.com", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6390375, 35.7776225 ] } }, 44 | { "type": "Feature", "properties": { "@id": "node\/2729883319", "name": "Bu Ku", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6382131, 35.7754027 ] } }, 45 | { "type": "Feature", "properties": { "@id": "node\/2731382547", "name": "Caffe Luna", "phone": null, "website": null, "cuisine": "italian" }, "geometry": { "type": "Point", "coordinates": [ -78.6367632, 35.7780662 ] } }, 46 | { "type": "Feature", "properties": { "@id": "node\/2734428056", "name": "Dickey's Barbecue Pit", "phone": null, "website": null, "cuisine": "regional" }, "geometry": { "type": "Point", "coordinates": [ -78.637703, 35.7754236 ] } }, 47 | { "type": "Feature", "properties": { "@id": "node\/2782524126", "name": "Chuck's", "phone": null, "website": null, "cuisine": "burger" }, "geometry": { "type": "Point", "coordinates": [ -78.638187, 35.77711 ] } }, 48 | { "type": "Feature", "properties": { "@id": "node\/2782526003", "name": "Beasley's Chicken and Honey", "phone": null, "website": null, "cuisine": "chicken" }, "geometry": { "type": "Point", "coordinates": [ -78.6382, 35.777028 ] } }, 49 | { "type": "Feature", "properties": { "@id": "node\/2782527413", "name": "Gravy Italian-American Kitchen", "phone": "+1 919-896-8513", "website": "http:\/\/www.gravyraleigh.com\/", "cuisine": "italian" }, "geometry": { "type": "Point", "coordinates": [ -78.6380687, 35.7784984 ] } }, 50 | { "type": "Feature", "properties": { "@id": "node\/2921397005", "name": "Blue Mango", "phone": "+1 919 3222760", "website": "http:\/\/www.bluemangoraleigh.com\/", "cuisine": "indian" }, "geometry": { "type": "Point", "coordinates": [ -78.6472129, 35.7840277 ] } }, 51 | { "type": "Feature", "properties": { "@id": "node\/2921397017", "name": "Rye Bar & Southern Kitchen", "phone": "+1-919-227-3370", "website": "http:\/\/www.ryeraleigh.com", "cuisine": "american" }, "geometry": { "type": "Point", "coordinates": [ -78.6396822, 35.7736589 ] } }, 52 | { "type": "Feature", "properties": { "@id": "node\/2921397020", "name": "Sam & Wally's", "phone": "+1 919 8297215", "website": "http:\/\/www.samandwallys.com\/", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6405676, 35.7744554 ] } }, 53 | { "type": "Feature", "properties": { "@id": "node\/2921397021", "name": "Sono", "phone": "+1-919-521-5328", "website": "http:\/\/www.sonoraleigh.com\/", "cuisine": "japanese" }, "geometry": { "type": "Point", "coordinates": [ -78.6391293, 35.7759268 ] } }, 54 | { "type": "Feature", "properties": { "@id": "node\/2941810252", "name": "Jet's Pizza", "phone": null, "website": "www.jetspizza.com", "cuisine": "pizza" }, "geometry": { "type": "Point", "coordinates": [ -78.6203969, 35.8225812 ] } }, 55 | { "type": "Feature", "properties": { "@id": "node\/3011712773", "name": "On the Oval Culinary Creations", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6744446, 35.7702302 ] } }, 56 | { "type": "Feature", "properties": { "@id": "node\/3248742655", "name": "Twisted Mango", "phone": "+1-919-838-8700", "website": null, "cuisine": "caribbean" }, "geometry": { "type": "Point", "coordinates": [ -78.6391621, 35.7748818 ] } }, 57 | { "type": "Feature", "properties": { "@id": "node\/3248742656", "name": "Jimmy V's Osteria + Bar", "phone": "+1-919-256-1451", "website": "http:\/\/jimmyvsraleigh.com\/", "cuisine": "italian,_bar" }, "geometry": { "type": "Point", "coordinates": [ -78.6395982, 35.7747799 ] } }, 58 | { "type": "Feature", "properties": { "@id": "node\/3248742660", "name": "Zinda New Asian", "phone": "+1-919-825-0995", "website": "http:\/\/zindaraleigh.com", "cuisine": "asian" }, "geometry": { "type": "Point", "coordinates": [ -78.6390895, 35.776478 ] } }, 59 | { "type": "Feature", "properties": { "@id": "node\/3248743362", "name": "ORO Restaurant & Lounge", "phone": "+1-919-239-4010", "website": "http:\/\/www.ororaleigh.com", "cuisine": "american" }, "geometry": { "type": "Point", "coordinates": [ -78.6385031, 35.7767563 ] } }, 60 | { "type": "Feature", "properties": { "@id": "node\/3305442675", "name": "Berkeley Cafe", "phone": "+1 919-828-9190", "website": "https:\/\/www.facebook.com\/pages\/Berkeley-Cafe\/35414122239", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6428012, 35.7769453 ] } }, 61 | { "type": "Feature", "properties": { "@id": "node\/3305442676", "name": "Brewmasters Bar & Grill", "phone": "919-836-9338", "website": "http:\/\/brewmastersbarandgrill.com\/", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6436982, 35.7769655 ] } }, 62 | { "type": "Feature", "properties": { "@id": "node\/3307013373", "name": "Clyde Cooper's BBQ", "phone": "+1 919-832-7614", "website": "http:\/\/clydecoopersbbq.com\/", "cuisine": "regional" }, "geometry": { "type": "Point", "coordinates": [ -78.6381649, 35.7762672 ] } }, 63 | { "type": "Feature", "properties": { "@id": "node\/3311658569", "name": "The Remedy Diner", "phone": "919.835.3553", "website": "http:\/\/www.theremedydiner.com\/", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6369163, 35.7783054 ] } }, 64 | { "type": "Feature", "properties": { "@id": "node\/3311658570", "name": "Troy Mezze Lounge", "phone": "+1 919-834-8133", "website": "http:\/\/www.troymezze.com\/", "cuisine": "turkish" }, "geometry": { "type": "Point", "coordinates": [ -78.6356915, 35.7762867 ] } }, 65 | { "type": "Feature", "properties": { "@id": "node\/3311658572", "name": "Vic's Italian Restaurant", "phone": "+1 919-829-7090", "website": "http:\/\/www.vicsitalianrestaurant.com\/", "cuisine": "italian" }, "geometry": { "type": "Point", "coordinates": [ -78.63571, 35.7758905 ] } }, 66 | { "type": "Feature", "properties": { "@id": "node\/3311663688", "name": "Centro", "phone": "+1 919-835-3593", "website": "http:\/\/www.centroraleigh.com\/", "cuisine": "mexican" }, "geometry": { "type": "Point", "coordinates": [ -78.6383213, 35.7792709 ] } }, 67 | { "type": "Feature", "properties": { "@id": "node\/3501372516", "name": "Bloomsbury Bistro", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6446015, 35.8042192 ] } }, 68 | { "type": "Feature", "properties": { "@id": "node\/3501375480", "name": "Lilly's Pizza", "phone": null, "website": null, "cuisine": "pizza" }, "geometry": { "type": "Point", "coordinates": [ -78.6466653, 35.8050808 ] } }, 69 | { "type": "Feature", "properties": { "@id": "node\/3590210985", "name": "Ba-Da Wings", "phone": "+1-919-832-3902", "website": "http:\/\/www.badawings.com\/", "cuisine": "Wings" }, "geometry": { "type": "Point", "coordinates": [ -78.6758885, 35.7784182 ] } }, 70 | { "type": "Feature", "properties": { "@id": "node\/3623985409", "name": "State of Beer", "phone": "+1-919-546-9116", "website": "http:\/\/stateof.beer\/", "cuisine": "sandwich" }, "geometry": { "type": "Point", "coordinates": [ -78.6453497, 35.7804078 ] } }, 71 | { "type": "Feature", "properties": { "@id": "node\/3656191609", "name": "Jade Garden", "phone": "+1-919-594-1813", "website": null, "cuisine": "chinese" }, "geometry": { "type": "Point", "coordinates": [ -78.6556757, 35.7828092 ] } }, 72 | { "type": "Feature", "properties": { "@id": "node\/3691460067", "name": "Shuckers Oyster Bar & Grill", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6468158, 35.7871261 ] } }, 73 | { "type": "Feature", "properties": { "@id": "node\/3691470161", "name": "Big Boom", "phone": null, "website": null, "cuisine": "italian" }, "geometry": { "type": "Point", "coordinates": [ -78.6470894, 35.7871021 ] } }, 74 | { "type": "Feature", "properties": { "@id": "node\/3691476947", "name": "Carolina Ale House", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6470814, 35.7865603 ] } }, 75 | { "type": "Feature", "properties": { "@id": "node\/3691477972", "name": "Dos Taquitos", "phone": null, "website": null, "cuisine": "mexican" }, "geometry": { "type": "Point", "coordinates": [ -78.6471538, 35.7854833 ] } }, 76 | { "type": "Feature", "properties": { "@id": "node\/3691491947", "name": "Plates Kitchen", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6476301, 35.7843429 ] } }, 77 | { "type": "Feature", "properties": { "@id": "node\/3796331874", "name": "Death & Taxes", "phone": "+1-984-242-0218", "website": "http:\/\/ac-restaurants.com\/death-taxes\/", "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6404347, 35.7782232 ] } }, 78 | { "type": "Feature", "properties": { "@id": "node\/3840045003", "name": "Gonza Tacos Y Tequila", "phone": "+1-919-268-8965", "website": "http:\/\/www.gonzatacosytequila.com", "cuisine": "mexican" }, "geometry": { "type": "Point", "coordinates": [ -78.663733, 35.7867649 ] } }, 79 | { "type": "Feature", "properties": { "@id": "node\/3871422566", "name": "Noodles & Company", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6616737, 35.7893909 ] } }, 80 | { "type": "Feature", "properties": { "@id": "node\/3871422567", "name": "Cafe Carolina", "phone": null, "website": null, "cuisine": null }, "geometry": { "type": "Point", "coordinates": [ -78.6614989, 35.7891548 ] } } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /data/states_21basic.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/data/states_21basic.zip -------------------------------------------------------------------------------- /exercise1_getting_set_up.md: -------------------------------------------------------------------------------- 1 | # Exercise 1, Getting Set Up 2 | 3 | This workshop takes place completely in the browser, using web-based tools and platforms that are available for anyone to leverage. We just need to do a couple quick set-up steps to get started in building our web-map. 4 | 5 | ### Accounts 6 | In order to fully leverage the tools that are available to us we need to have accounts in two places, [GitHub](https://www.github.com) and [MapBox](https://www.mapbox.com). Go ahead and sign up for those services, I'll wait. 7 | 8 | Oh and make sure that you confirm your e-mail address for GitHub or the publishing tools that we're going to use won't work very well. 9 | 10 | Ok, good, let's go to [exercise 2](/exercise2_fork_me.md). 11 | -------------------------------------------------------------------------------- /exercise2_fork_me.md: -------------------------------------------------------------------------------- 1 | # Exercise 2, Fork Me 2 | 3 | Since we're doing a web-map, we have to have a place on the web to host our map. Luckily GitHub provides us with a very easy way to publish public-facing websites. They call it [GH Pages](https://pages.github.com/). 4 | 5 | Basically it works by publishing anything you put in a branch called "gh-pages" in your repository as a static site. You might notice that the default branch in this repository is set to gh-pages, that means it's already being published as a web site. 6 | 7 | Curious what it looks like? Check it out here: 8 | 9 | [https://willbreitkreutz.github.io/web_mapping_workshop/](https://willbreitkreutz.github.io/web_mapping_workshop/) 10 | 11 | Now that's one ugly map isn't it! Well, you're going to do something about that. 12 | 13 | You don't have edit rights on my version of the map, so you are going to make a copy of this entire repository under your account, this is called _forking_ in the GitHub world. 14 | 15 | To fork the repository into your own copy, just hit the fork button at the top of the page: 16 | 17 | ![fork_me.png](/img/fork_me.png) 18 | 19 | Awesome, now you have a complete copy of the workshop in your profile. 20 | 21 | Before we get into the code, let's get a better base map set up, that green is too ugly to let slide. [Exercise 3, Base Maps](/exercise3_base_maps.md) 22 | -------------------------------------------------------------------------------- /exercise3_base_maps.md: -------------------------------------------------------------------------------- 1 | # Exercise 3, Base Maps 2 | 3 | That ugly green map is being provided by MapBox, one of the leaders in open source mapping right now, and arguably the best base map provider out there. They have a number of tools that allow you to create and publish your own base maps using [OpenStreetMap](https://www.openstreetmap.org) data as their backbone. 4 | 5 | The tools that they make available vary in complexity with the amount of flexibility and customizability that they provide. We could spend an entire workshop and more talking about how to author maps from scratch, lucky for us they also let you start from one of their out of the box themes and create your own. 6 | 7 | With the free account, you get 50,000 map views / month which is way more than enough for personal projects. 8 | 9 | Check out their new tools for authoring base maps [here](https://www.mapbox.com/maps/). 10 | 11 | ## Build a better base map 12 | 13 | At the [MapBox home page](https://www.mapbox.com) login to your account if you aren't already. This will take you to your account home page: 14 | 15 | ![home_page.png](/img/home_page.png) 16 | 17 | We're going to use the simple MapBox Editor tool instead of the more complex MapBox Studio since we don't have the time to get that deep into it, you should definitely try out Studio to experience the amount of detail you can put into the design. 18 | 19 | From your account home page, click on the Classic button in the left sidebar to open the classic tools: 20 | 21 | ![classic.png](/img/classic.png) 22 | 23 | Then click on the Editor projects tab: 24 | 25 | ![editor_projects.png](/img/editor_projects.png) 26 | 27 | Then you can click to create a new MapBox Editor project: 28 | 29 | ![new_project.png](/img/new_project.png) 30 | 31 | Choose from one of the standard base maps that they provide. You used to be able to do some slight customization in this interface, but they would like you to use one of their Studio products to make more custom base maps. For this workshop we'll settle for one of the out of the box themes. 32 | 33 | ![base_maps.png](/img/base_maps.png) 34 | 35 | You can add or create data that MapBox will publish for you as part of your layer here as well, but we're going to be doing that later on in the workshop so let's hold off on that for now. 36 | 37 | Save your project and then go back to your account page by clicking the little profile icon at the upper left of the screen: 38 | 39 | ![profile.png](/img/profile.png) 40 | 41 | Your new project will be listed on the page along with a map ID, remember where this is because we'll be needing it in the next exercise. 42 | 43 | ![project.png](/img/project.png) 44 | 45 | 46 | Alright, let's start doing some coding! [Exercise 4, A basic map](/exercise4_a_basic_map.md) 47 | -------------------------------------------------------------------------------- /exercise4_a_basic_map.md: -------------------------------------------------------------------------------- 1 | # Exercise 4, A Basic Map 2 | 3 | We've already looked at how GitHub lets us store code and publish web sites, now let's get rid of that ugly green map and publish a basic mapping site with the base map you created in the last exercise. 4 | 5 | ## Editor of the Day 6 | 7 | Now you could download all of the code for the project to your local machine, make the edits and push it back to GitHub for deployment, that would work. We're going to short cut that workflow and keep all of our work in the browser! 8 | 9 | [Prose.io](http://prose.io) is an awesome little tool that lets you access and edit files hosted in GitHub. It's designed for editing blog posts in [Markdown](https://en.wikipedia.org/wiki/Markdown), but works just as well for doing quick edits in your code. I wouldn't use it for building a large application, but for the app we're putting together it will work beautifully. You will need to authorize the Prose app using your GitHub account, but after that access is pretty seamless. 10 | 11 | ## Explore the Project 12 | 13 | Since you forked my project, you now have a complete working copy of the ugly green map that I built out. Check it out at 14 | 15 | https://www. {your GitHub user name} .github.io/web_mapping_workshop/ 16 | 17 | Is the map a ugly shade of green? Great! Keep that link handy, that's where you're going to build your app. 18 | 19 | Let's go back to Prose.io to explore how that map works. 20 | 21 | Open the index.html file at the root directory. 22 | 23 | Let's break it down into the major parts: 24 | 25 | ```html 26 | 27 | 28 | 29 | 30 | 31 | A simple map 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ``` 42 | 43 | The top part of the file, or the `` section contains links to all of the css and javascript libraries that the app needs to function, these are the 3rd party files that we're leveraging without having to write ourselves. 44 | 45 | ```html 46 | 47 | 48 | 49 | 50 |
51 | 52 | 53 | ``` 54 | 55 | The body of the .html file begins now. We've got a pretty simple file here, the only markup we need are two elements, one to hold our map, and another that we're going to use in exercise 7. 56 | 57 | ```html 58 | 59 | 60 | 61 | 62 | 68 | 69 | ``` 70 | 71 | Our JavaScript comes after the markup, toward the bottom of the file. This ensures that the browser is done reading and understanding the DOM structure before we start leveraging it to create an application. 72 | 73 | Notice how the exercise 4 file is the only uncommented one. This way if you happen to fall behind as we move through exercises, you can just uncomment the relavent file, but I'd rather you write all your code in the exercise 4 file as we build the app. 74 | 75 | ```html 76 | 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | ``` 88 | 89 | At the bottom of the page is the special bonus section, we'll cover what it does if we have time at the end of the workshop. Also, you can check out [special_bonus.md](/special_bonus.md) later if we don't quite get there. 90 | 91 | ## Get Rid of the Ugly! 92 | 93 | Ok, now let's get rid of that ugly green map. 94 | 95 | Open `js/exercise_4.js` in Prose.io. It should look something like this: 96 | 97 | ```javascript 98 | 99 | // Here is the javascript setup for a basic map: 100 | 101 | // Enter your mapbox map id here to reference it for the base layer, 102 | // this one references the ugly green map that I made. 103 | var mapId = 'will-breitkreutz.k6fj4l3f'; 104 | 105 | // And this is my access token, use yours. 106 | var accessToken = 'pk.eyJ1Ijoid2lsbC1icmVpdGtyZXV0eiIsImEiOiItMTJGWEF3In0.HEvuRMMVxBVR5-oDYvudxw'; 107 | 108 | // Create the map object with your mapId and token, 109 | // referencing the DOM element where you want the map to go. 110 | L.mapbox.accessToken = accessToken; 111 | var map = L.mapbox.map('map', mapId); 112 | 113 | // Set the initial view of the map to the whole US 114 | map.setView([39, -96], 4); 115 | 116 | // Great, now we have a basic web map! 117 | 118 | ``` 119 | 120 | You'll notice that I've inluded a map ID and access token that are unique to my MapBox account, and that green map. 121 | 122 | Replace the map ID and access token with your map ID from the map you just created and the access token from your MapBox account: 123 | 124 | The map ID is located with the layer you created: 125 | 126 | ![project.png](/img/project.png) 127 | 128 | Your access token is located in the home page of your MapBox account in the right sidebar in a section that looks like this: 129 | 130 | ![access_token.png](/img/access_token.png) 131 | 132 | Copy each of these and replace the ones in `js/exercise_4.js`. 133 | 134 | Go to your map link and check out your map, I'll bet it's looking better already! 135 | 136 | We'll level up and add some Thematic data to the map in the next [exercise](/exercise5_put_some_data_on_the_map.md) 137 | -------------------------------------------------------------------------------- /exercise5_put_some_data_on_the_map.md: -------------------------------------------------------------------------------- 1 | # Exercise 5, Put Some Data on the Map 2 | 3 | We now have a fully functional (if pretty boring) mapping app in just a few lines of code. 4 | 5 | _Contents of js/exercise4.js_ 6 | ```javascript 7 | // Here is the javascript setup for a basic map: 8 | 9 | // Enter your mapbox map id here to reference it for the base layer, 10 | // this one references the ugly green map that I made. 11 | var mapId = 'will-breitkreutz.k6fj4l3f'; 12 | 13 | // And this is my access token, use yours. 14 | var accessToken = 'pk.eyJ1Ijoid2lsbC1icmVpdGtyZXV0eiIsImEiOiItMTJGWEF3In0.HEvuRMMVxBVR5-oDYvudxw'; 15 | 16 | // Create the map object with your mapId and token, 17 | // referencing the DOM element where you want the map to go. 18 | L.mapbox.accessToken = accessToken; 19 | var map = L.mapbox.map('map', mapId); 20 | 21 | // Set the initial view of the map to the whole US 22 | map.setView([39, -96], 4); 23 | 24 | // Great, now we have a basic web map! 25 | ``` 26 | 27 | ## Finding some data 28 | 29 | Let's liven up the map and put some data on it. 30 | 31 | At this point what we need is a dataset in [geojson](www.geojson.org) format. There are a number of places to find data or ways to convert your own data into geojson. A couple of places to find data include these below, there are tons of data out there though, it just takes a little digging: 32 | 33 | _In the live workshop we'll walk through extracting some data from overpass-turbo_ 34 | 35 | 1. Download some at [overpass-turbo](http://overpass-turbo.eu/) 36 | 2. Make your own at [geojson.io](http://www.geojson.io) 37 | 38 | For the lazy out there, I've also included a couple of datasets in this repository, you can find them in the /data folder. Notice that if you look at the datasets in GitHub, it will let you preview the data on a map, pretty cool huh. 39 | 40 | We're going to use the restaurants.geojson dataset for the tutorial. 41 | 42 | ## Add GeoJSON to the map 43 | 44 | Go to prose.io and open your js/exercise4.js file, we're going to make some edits. 45 | 46 | At the end of the file, add the following code: 47 | 48 | _We need to set the path to our data file to a variable, replace restaurants.geojson with the data file that you want to use_ 49 | ```javascript 50 | var dataFileToAdd = 'data/restaurants.geojson'; 51 | ``` 52 | 53 | _Then we need to create a featureLayer to hold the data_ 54 | ```javascript 55 | var featureLayer = L.mapbox.featureLayer() 56 | .loadURL(dataFileToAdd) 57 | .addTo(map); 58 | ``` 59 | 60 | _Finally we're going to set the style and zoom the map to the layer once the featureLayer is ready to render_ 61 | ```javascript 62 | featureLayer.on('ready', function() { 63 | this.eachLayer(function(layer){ 64 | layer.setIcon(L.mapbox.marker.icon({ 65 | 'marker-color': '#8834bb', 66 | 'marker-size': 'large', 67 | 'marker-symbol': 'restaurant' 68 | })) 69 | }); 70 | map.fitBounds(featureLayer.getBounds()); 71 | }); 72 | ``` 73 | 74 | That's it, you've added data to your map! Feel free to change any of the style information to make it look the best it can on top of your basemap. 75 | 76 | Go ahead and open the map up in another tab and double check that you now have data on top of your basemap. If you do, time to move on to the next [exercise](/exercise6_adding_basic_popups.md) 77 | -------------------------------------------------------------------------------- /exercise6_adding_basic_popups.md: -------------------------------------------------------------------------------- 1 | # Exercise 6, Adding Basic Popups 2 | 3 | Ok, data on the map is cool, but how about we make it a little more interactive by adding popups when you click on a feature. 4 | 5 | This is actually pretty easy since our mapping library has popups built in. 6 | 7 | We just need to add the following bit of JS code: 8 | 9 | ```javascript 10 | featureLayer.on('ready', function(){ 11 | this.eachLayer(function(layer){ 12 | layer.bindPopup('Welcome to ' + layer.feature.properties.name); 13 | }); 14 | }); 15 | ``` 16 | 17 | This adds another callback to run when the featureLayer is ready to go, this function loops through each feature, called a 'layer' in the code and adds a popup that presents the user with the name of the restaurant they clicked on. If you chose to use a different layer you will need to edit the popup contents to reference valid data from that layer. 18 | 19 | Once you get the popups working and showing your users some useful information we can move to the next [exercise](/exercise7_fancy_click_handling.md) 20 | -------------------------------------------------------------------------------- /exercise7_fancy_click_handling.md: -------------------------------------------------------------------------------- 1 | # Exercise 7, Fancy Click Handling 2 | 3 | Popups are cool, but what if we want a little more professional looking interaction when users click on our data? 4 | 5 | I've snuck in a panel that's been invisible so far that overlays on top of the map at the right side, we're going to put our data in that panel instead of in the popup when the user clicks on a feature. 6 | 7 | If you look at `index.html` you'll see right next to the `
` called 'map' there's a `
` with the ID of 'sidebar' that contains a couple of other nodes, that's our overlay. We've also included a little css to position and style it to our liking: 8 | 9 | ```css 10 | #sidebar { 11 | position:absolute; top: 10px; right: 10px; bottom: 10px; width: 260px; 12 | background:#333; color: #fff; 13 | padding:20px; 14 | font-family: Arial, Helvetica, sans-serif; 15 | opacity:0.9; 16 | filter:alpha(opacity=80); /* For IE8 and earlier */ 17 | } 18 | ``` 19 | ## Clean up from earlier 20 | 21 | Unless we want popups _and_ the overlay box, we should either delete or comment out the first click handler that we added to our layer in the last exercise: 22 | 23 | ```javascript 24 | //featureLayer.on('ready', function(){ 25 | // this.eachLayer(function(layer){ 26 | // layer.bindPopup('Welcome to ' + layer.feature.properties.name); 27 | // }); 28 | //}); 29 | ``` 30 | 31 | Rather than use the out-of-the-box popup binding, we're going to write a little more code to handle the click ourselves. 32 | 33 | Start by adding a click handler function that takes the click event as a variable called 'e': 34 | 35 | ```javascript 36 | var clickHandler = function(e){ 37 | 38 | }; 39 | ``` 40 | 41 | When the user clicks on a new feature we want to make sure any old data is removed, we're going to use [jQuery](https://jquery.com/) to do any of the actual DOM interaction because they make it super easy. Everywhere you see a `$` we're using the jQuery library. 42 | 43 | Using the jQuery selector for any node with the ID of 'info' (that's where we're going to put some info about the point that the user clicks on) we make sure to remove any child elements. 44 | 45 | ```javascript 46 | var clickHandler = function(e){ 47 | $('#info').empty(); 48 | }; 49 | ``` 50 | 51 | Now we want to get a reference to the feature that the user clicked on. Lucky for us, the click event `e` references the target of the click and we can access the feature object from there and assign it to a variable called `feature`: 52 | 53 | ```javascript 54 | var clickHandler = function(e){ 55 | $('#info').empty(); 56 | 57 | var feature = e.target.feature; 58 | }; 59 | ``` 60 | 61 | Now we can show our info element and add text from the attributes of our feature. jQuery lets us fade an element in over time to make it a little more pleasing of an interaction so rather than just use the `.show()` function, we'll use `.fadeIn()` to make it appear in 400 milliseconds. 62 | 63 | ```javascript 64 | var clickHandler = function(e){ 65 | $('#info').empty(); 66 | 67 | var feature = e.target.feature; 68 | 69 | $('#sidebar').fadeIn(400, function(){ 70 | 71 | } 72 | }; 73 | ``` 74 | 75 | The `.fadeIn()` function allows us to provide a callback that we can use to add elements to the panel once it's ready. We're going to create a string variable and populate it using properties from our feature, if you used a dataset other than restaurants.geojson the feature names will need to be changed. 76 | 77 | ```javascript 78 | var clickHandler = function(e){ 79 | $('#info').empty(); 80 | 81 | var feature = e.target.feature; 82 | 83 | $('#sidebar').fadeIn(400,function(){ 84 | var info = ''; 85 | 86 | info += '
' 87 | info += '

' + feature.properties.name + '

' 88 | if(feature.properties.phone) info += '

' + feature.properties.cuisine + '

' 89 | if(feature.properties.phone) info += '

' + feature.properties.phone + '

' 90 | if(feature.properties.phone) info += '

' + feature.properties.website + '

' 91 | if(feature.properties.phone) info += '

' + feature.properties.website + '

' 92 | info += '
' 93 | 94 | $('#info').append(info); 95 | }); 96 | }; 97 | ``` 98 | 99 | As the last step we append the string content to our info panel using the `.append()` function. 100 | 101 | To register our click handler so that it is actually called when a user clicks on a feature we're going to add a listener to the click event on each of the features in our featureLayer once it's been fully registered. 102 | 103 | ```javascript 104 | var clickHandler = function(e){ 105 | $('#info').empty(); 106 | 107 | var feature = e.target.feature; 108 | 109 | $('#sidebar').fadeIn(400,function(){ 110 | var info = ''; 111 | 112 | info += '
' 113 | info += '

' + feature.properties.name + '

' 114 | if(feature.properties.phone) info += '

' + feature.properties.cuisine + '

' 115 | if(feature.properties.phone) info += '

' + feature.properties.phone + '

' 116 | if(feature.properties.phone) info += '

' + feature.properties.website + '

' 117 | if(feature.properties.phone) info += '

' + feature.properties.website + '

' 118 | info += '
' 119 | 120 | $('#info').append(info); 121 | }); 122 | }; 123 | 124 | featureLayer.on('ready', function(){ 125 | this.eachLayer(function(layer){ 126 | layer.on('click', clickHandler); 127 | }); 128 | }); 129 | ``` 130 | 131 | Nice! Now we have an overlay that is populated with properties from the feature that the user clicked on, how do we get rid of it when we're done? 132 | 133 | We need to add on more little bit of code so that the info panel is hidden when a user clicks anywhere else on the map so that it can be dismissed by fading it out and emptying it of any content. 134 | 135 | ```javascript 136 | var clickHandler = function(e){ 137 | $('#info').empty(); 138 | 139 | var feature = e.target.feature; 140 | 141 | $('#sidebar').fadeIn(400,function(){ 142 | var info = ''; 143 | 144 | info += '
' 145 | info += '

' + feature.properties.name + '

' 146 | if(feature.properties.phone) info += '

' + feature.properties.cuisine + '

' 147 | if(feature.properties.phone) info += '

' + feature.properties.phone + '

' 148 | if(feature.properties.phone) info += '

' + feature.properties.website + '

' 149 | if(feature.properties.phone) info += '

' + feature.properties.website + '

' 150 | info += '
' 151 | 152 | $('#info').append(info); 153 | }); 154 | }; 155 | 156 | featureLayer.on('ready', function(){ 157 | this.eachLayer(function(layer){ 158 | layer.on('click', clickHandler); 159 | }); 160 | }); 161 | 162 | map.on('click',function(e){ 163 | $('#info').fadeOut(200); 164 | $('#info').empty(); 165 | }); 166 | ``` 167 | 168 | ## Web Map Success!! 169 | 170 | Let's have some more fun, how about putting a point on the map based on our users location? If you've got time, check out [exercise 8](/exercise8_bonus_locate_me.md) 171 | -------------------------------------------------------------------------------- /exercise8_bonus_locate_me.md: -------------------------------------------------------------------------------- 1 | # Exercise 8, Bonus Round! Locate Me 2 | 3 | Most browsers have the ability to get the location of the device that they are running on from a variety of sources from GPS to wifi and physical networks. We can use this information to place a point on the map and zoom the user there so they can quickly make the map relevant to their current location. Of course the browser has to get permission from the user before we can access this data. You can read more on the [Geolocation API](https://en.wikipedia.org/wiki/W3C_Geolocation_API) on wikipedia. 4 | 5 | Let's start by creating a variable to hold the current location: 6 | 7 | ```javascript 8 | var myLocation = L.mapbox.featureLayer().addTo(map); 9 | ``` 10 | 11 | We created a blank featureLayer and added it to the map, it won't show up or do anything since there's no location information there yet. 12 | 13 | The MapBox.js map mapping library gives a simple interface for querying the users location, we just need to call `map.locate()`. The only problem is the we also need to tell the app what to do once we get a location. 14 | 15 | Let's write a handler function to tell the map to add an icon wherever the user is located: 16 | 17 | ```javascript 18 | map.on('locationfound', function(e) { 19 | 20 | myLocation.setGeoJSON({ 21 | type: 'Feature', 22 | geometry: { 23 | type: 'Point', 24 | coordinates: [e.latlng.lng, e.latlng.lat] 25 | }, 26 | properties: { 27 | 'title': 'Here I am!', 28 | 'marker-color': '#ff8888', 29 | 'marker-symbol': 'star' 30 | } 31 | }); 32 | 33 | }); 34 | ``` 35 | 36 | Now we can call `map.locate()` to add the users location to the map when the app starts up: 37 | 38 | ```javascript 39 | map.locate({setView:true}); 40 | ``` 41 | 42 | #Congratulations! 43 | 44 | You've made a pretty great start on developing a mapping application and didn't event have to leave the comfort of the browser to do it! 45 | 46 | If you've still got time, check out the super bonus round, [exercise 9](/exercise9_super_bonus_add_directions.md). 47 | -------------------------------------------------------------------------------- /exercise9_super_bonus_add_directions.md: -------------------------------------------------------------------------------- 1 | # Exercise 9, Super Bonus Round! Directions API 2 | 3 | We already have a nice mapping application, but what if we added some even fancier functionality. We're mapping restaurants, and our location, seems reasonable that we would want to figure out how to get from where we are now, to the restaurant of our choice! 4 | 5 | This is going to be a more complex exercise, but if you made it this far, I know you're gonna do great! 6 | 7 | ## Mapzen Directions API 8 | 9 | Mapzen is a spatial software and service company that's doing some awesome work with open datasets. They've put together a few API's that make it super easy to do geocoding and direction finding. We're going to use their [Mapzen Turn-by-Turn](https://mapzen.com/projects/turn-by-turn) project to get directions to our restaurants. 10 | 11 | In order to access their API, you need to register for an API Key. You can register with Mapzen by authorizing access through your GitHub account, which makes it super easy. I've created an API key for this exercise, it's on the free tier and won't every be upgraded, and might be removed by the time you get here... so you should get your own key if you want to use this or any of their other services in the future. 12 | 13 | ## Requesting Directions 14 | 15 | By reading the documentation we found out that to request directions, we need to make a call to `http://valhalla.mapzen.com/route` (best name ever in my opinion) with some query parameters describing what kind of information we're looking for. 16 | 17 | Let's start by creating a function that we can call with from and to locations that will eventually add our route to the map. 18 | 19 | So at the end of our JS file, let's start to add some code: 20 | 21 | ```javascript 22 | function getDirections(frm, to){ 23 | 24 | } 25 | ``` 26 | 27 | We know that to get our directions, we need to make a GET request with a JSON payload describing our query. Since we're using JQuery in our project we can leverage their AJAX request functionality to make the request, but first, let's build our JSON query object: 28 | 29 | ```javascript 30 | function getDirections(frm, to){ 31 | var jsonPayload = JSON.stringify({ 32 | locations:[ 33 | {lat: frm[1], lon: frm[0]}, 34 | {lat: to[1], lon: to[0]} 35 | ], 36 | costing: 'pedestrian', 37 | units: 'miles' 38 | }) 39 | } 40 | ``` 41 | 42 | We have to run `JSON.stringify()` on the JSON to convert it to a string that can be sent as a query parameter in the URL string. 43 | 44 | Now let's add the AJAX code that will make our request for us: 45 | 46 | ```javascript 47 | function getDirections(frm, to){ 48 | var jsonPayload = JSON.stringify({ 49 | locations:[ 50 | {lat: frm[1], lon: frm[0]}, 51 | {lat: to[1], lon: to[0]} 52 | ], 53 | costing: 'pedestrian', 54 | units: 'miles' 55 | }) 56 | $.ajax({ 57 | url:'http://valhalla.mapzen.com/route', 58 | data:{ 59 | json: jsonPayload, 60 | api_key: 'valhalla-gwtf3x2' 61 | } 62 | }) 63 | } 64 | ``` 65 | 66 | ## Parsing the Response 67 | 68 | The code above will make the request, but nothing will happen unless we tell JQuery what to do with the response, we do that by chaining on a handler function using the `.done()` function of the promise-like object returned by the `$.ajax` function: 69 | 70 | ```javascript 71 | function getDirections(frm, to){ 72 | var jsonPayload = JSON.stringify({ 73 | locations:[ 74 | {lat: frm[1], lon: frm[0]}, 75 | {lat: to[1], lon: to[0]} 76 | ], 77 | costing: 'pedestrian', 78 | units: 'miles' 79 | }) 80 | $.ajax({ 81 | url:'http://valhalla.mapzen.com/route', 82 | data:{ 83 | json: jsonPayload, 84 | api_key: 'valhalla-gwtf3x2' 85 | } 86 | }).done(function(data){ 87 | 88 | }) 89 | } 90 | ``` 91 | 92 | ## Add our route to the map 93 | 94 | Wait, what do we want to do with our directions data? Let's create a route feature layer so that we can display the route line on the map, to do that we want to create the feature layer before we create our `getDirections()` function, and add the empty featureLayer to the map so when we add data to it later, it will automatically show up for our user: 95 | 96 | ```javascript 97 | var routeLine = L.mapbox.featureLayer().addTo(map); 98 | function getDirections(frm, to){ 99 | var jsonPayload = JSON.stringify({ 100 | locations:[ 101 | {lat: frm[1], lon: frm[0]}, 102 | {lat: to[1], lon: to[0]} 103 | ], 104 | costing: 'pedestrian', 105 | units: 'miles' 106 | }) 107 | $.ajax({ 108 | url:'http://valhalla.mapzen.com/route', 109 | data:{ 110 | json: jsonPayload, 111 | api_key: 'valhalla-gwtf3x2' 112 | } 113 | }).done(function(data){ 114 | 115 | }) 116 | } 117 | ``` 118 | 119 | Ok, now we can do something with the response data. The turn-by-turn API returns our route in an encoded geometry to save bandwidth rather than in straight-up GeoJSON. This means we have to decode it before we can use it. Luckily, Mapzen provides a [decode function](https://mapzen.com/documentation/turn-by-turn/decoding/) that we can borrow to do this for us, reading the docs FTW!: 120 | 121 | ```javascript 122 | var routeLine = L.mapbox.featureLayer().addTo(map); 123 | function getDirections(frm, to){ 124 | var jsonPayload = JSON.stringify({ 125 | locations:[ 126 | {lat: frm[1], lon: frm[0]}, 127 | {lat: to[1], lon: to[0]} 128 | ], 129 | costing: 'pedestrian', 130 | units: 'miles' 131 | }) 132 | $.ajax({ 133 | url:'http://valhalla.mapzen.com/route', 134 | data:{ 135 | json: jsonPayload, 136 | api_key: 'valhalla-gwtf3x2' 137 | } 138 | }).done(function(data){ 139 | var routeShape = polyline.decode(data.trip.legs[0].shape); 140 | routeLine.setGeoJSON({ 141 | type:'Feature', 142 | geometry:{ 143 | type:'LineString', 144 | coordinates: routeShape 145 | }, 146 | properties:{ 147 | "stroke": '#ed23f1', 148 | "stroke-opacity": 0.8, 149 | "stroke-width": 8 150 | } 151 | }) 152 | }) 153 | } 154 | ``` 155 | 156 | Awesome, now we're adding our route from the API response to the routeLine featureLayer! But wait, how do we actually call the `getDirections()` function? We need to go back up to where we handle the click event on a restaurant feature and make sure that we call `getDirections()` to add the route to the map: 157 | 158 | ```javascript 159 | //Back in lines 53-56ish 160 | $('#info').append(info); 161 | }); 162 | 163 | var myGeoJSON = myLocation.getGeoJSON(); 164 | 165 | getDirections(myGeoJSON.geometry.coordinates, feature.geometry.coordinates); 166 | ``` 167 | 168 | We're adding the call to `getDirections()` using the users location from `myGeoJSON` as the from location and the location of the restaurant that was clicked on as the to location. 169 | 170 | Time to test, go ahead and reload the application and see if you are seeing routes show up between your location and the restaurant that you click on. 171 | 172 | ## Add Directions to the Sidebar 173 | 174 | That's pretty cool and all, but what if we want to give the user a list of directions along with the route on the map? We've got a good place for it in the right sidebar, under the restaurant information. 175 | 176 | In this example app, I've already created a place for the directions to go, we just need to parse them out of the response and add them to the UI. 177 | 178 | To start, let's fade our directions region into being using JQuery, so inside our ajax data handler function, after we `setGeoJSON` on our featureLayer, let's add some code: 179 | 180 | ```javascript 181 | var routeLine = L.mapbox.featureLayer().addTo(map); 182 | function getDirections(frm, to){ 183 | var jsonPayload = JSON.stringify({ 184 | locations:[ 185 | {lat: frm[1], lon: frm[0]}, 186 | {lat: to[1], lon: to[0]} 187 | ], 188 | costing: 'pedestrian', 189 | units: 'miles' 190 | }) 191 | $.ajax({ 192 | url:'http://valhalla.mapzen.com/route', 193 | data:{ 194 | json: jsonPayload, 195 | api_key: 'valhalla-gwtf3x2' 196 | } 197 | }).done(function(data){ 198 | var routeShape = polyline.decode(data.trip.legs[0].shape); 199 | routeLine.setGeoJSON({ 200 | type:'Feature', 201 | geometry:{ 202 | type:'LineString', 203 | coordinates: routeShape 204 | }, 205 | properties:{ 206 | "stroke": '#ed23f1', 207 | "stroke-opacity": 0.8, 208 | "stroke-width": 8 209 | } 210 | }) 211 | 212 | $('#directions').fadeIn(400, function(){ 213 | 214 | }) 215 | 216 | }) 217 | } 218 | ``` 219 | 220 | Inside our `fadeIn()` callback is where we'll actually add the information to the DOM, (remember to empty the section of any previous directions as the first step). Let's start by adding the distance and travel time for the route: 221 | 222 | ```javascript 223 | $('#directions').fadeIn(400, function(){ 224 | $('#summary').empty(); 225 | $('#distance').text((Math.round(data.trip.summary.length * 100) / 100) + data.trip.units); 226 | $('#time').text((Math.round(data.trip.summary.time / 60 * 100) / 100) + ' min'); 227 | }) 228 | ``` 229 | 230 | That's pretty useful information, but what about each of the step-by-step directions? To add those, we'll loop over the maneuvers array and create a list item to add to our summary list that's already in the UI. We'll create the list item much like we create the DOM elements for the restaurant name and address we formatted earlier: 231 | 232 | ```javascript 233 | $('#directions').fadeIn(400, function(){ 234 | $('#summary').empty(); 235 | $('#distance').text((Math.round(data.trip.summary.length * 100) / 100) + data.trip.units); 236 | $('#time').text((Math.round(data.trip.summary.time / 60 * 100) / 100) + ' min'); 237 | 238 | data.trip.legs[0].maneuvers.forEach(function(item){ 239 | var direction = ''; 240 | direction += '
  • '; 241 | if(item.verbal_post_transition_instruction) direction += '

    ' + item.verbal_post_transition_instruction + '

    '; 242 | if(item.verbal_pre_transition_instruction) direction += '

    ' + item.verbal_pre_transition_instruction + '

    '; 243 | direction += '
  • '; 244 | $('#summary').append(direction); 245 | }) 246 | }) 247 | ``` 248 | 249 | Now we should be seeing a list of turn-by-turn directions show up in our right sidebar when we click on a restaurant! 250 | 251 | The only problem that we're having right now is that our route never goes away, even when the sidebar is removed by clicking on a blank area in the map. To fix this, we just need to add another click handler to the very end of our file that clears all of the data from our routeLine featureLayer: 252 | 253 | ```javascript 254 | // all the way to the end of our file 255 | map.on('click', function(){ 256 | routeLine.clearLayers(); 257 | }) 258 | ``` 259 | 260 | ## Highlight the route parts 261 | 262 | As a super-duper bonus, let's highlight the regions of the route that correspond to each of the turn-by-turn directions in our sidebar if the user mouses over them. 263 | 264 | Back inside our `.done()` handler, below the `$('#directions').fadeIn()` function call, let's add a handler for the mouseover event on each of our instructions: 265 | 266 | ```javascript 267 | $('.instruction').on('mouseover', function(){ 268 | 269 | }) 270 | ``` 271 | 272 | Each of our instructions contains references to the beginning and end index locations of the beginning and end coordinates for that section of the route in the overall coordinate array for the route. We added them as the `data-begin` and `data-end` attributes. Let's set those to variables that we can use to extract our small shape from the larger route: 273 | 274 | ```javascript 275 | $('.instruction').on('mouseover', function(){ 276 | var begin = Number($(this).attr('data-begin')); 277 | var end = Number($(this).attr('data-end')); 278 | }) 279 | ``` 280 | 281 | Now, how do we display our sub-route data? We'll create another featureLayer to hold the moused-over section of the route and call it `routeHighlight`, so go back up to where we create the routeLine and add the following line to create our routeHighlight: 282 | 283 | ```javascript 284 | var routeLine = L.mapbox.featureLayer().addTo(map); 285 | var routeHighlight = L.mapbox.featureLayer().addTo(map); //<-- 286 | function getDirections(frm, to){ 287 | ``` 288 | 289 | Awesome, now let's add our sub-route to the `routeHighlight` featureLayer on mouseover: (note that sometimes our sub-route is a single point so we should handle both points and line geometries) 290 | 291 | ```javascript 292 | $('.instruction').on('mouseover', function(){ 293 | var begin = Number($(this).attr('data-begin')); 294 | var end = Number($(this).attr('data-end')); 295 | 296 | routeHighlight.setGeoJSON({ 297 | type:'Feature', 298 | geometry:{ 299 | type: begin === end ? 'Point' : 'LineString', 300 | coordinates: begin === end ? routeShape.slice(begin)[0] : routeShape.slice(begin,(end + 1)) 301 | }, 302 | properties:{ 303 | "stroke": '#1ea6f2', 304 | "stroke-opacity": 0.9, 305 | "stroke-width": 10, 306 | "marker-color": '#1ea6f2', 307 | "marker-size": 'small', 308 | "marker-symbol": 'star' 309 | } 310 | }) 311 | }) 312 | ``` 313 | 314 | The only other thing we need to do is a little clean up, when the user mouses out of the sidebar, we should remove the highlight from the map. Add the following handler after our mouseover code: 315 | 316 | ```javascript 317 | $('.instruction').on('mouseout', function(){ 318 | routeHighlight.clearLayers() 319 | }) 320 | ``` 321 | 322 | Sweet, now see what else you can add to the application! Time to dig into the [docs and examples](https://www.mapbox.com/mapbox.js/api) and build away! 323 | -------------------------------------------------------------------------------- /images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/images/github.png -------------------------------------------------------------------------------- /images/load_folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/images/load_folder.png -------------------------------------------------------------------------------- /img/access_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/img/access_token.png -------------------------------------------------------------------------------- /img/base_maps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/img/base_maps.png -------------------------------------------------------------------------------- /img/classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/img/classic.png -------------------------------------------------------------------------------- /img/editor_projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/img/editor_projects.png -------------------------------------------------------------------------------- /img/fork_me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/img/fork_me.png -------------------------------------------------------------------------------- /img/home_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/img/home_page.png -------------------------------------------------------------------------------- /img/new_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/img/new_project.png -------------------------------------------------------------------------------- /img/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/img/profile.png -------------------------------------------------------------------------------- /img/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willbreitkreutz/web_mapping_workshop/8d84230562b6b314a4a4726b91a4bbd5aa29e1aa/img/project.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A simple map 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /js/exercise_4.js: -------------------------------------------------------------------------------- 1 | // Here is the javascript setup for a basic map: 2 | 3 | // Enter your mapbox map id here to reference it for the base layer, 4 | // this one references the ugly green map that I made. 5 | var mapId = 'will-breitkreutz.019h7bl7'; 6 | 7 | // And this is my access token, use yours. 8 | var accessToken = 'pk.eyJ1Ijoid2lsbC1icmVpdGtyZXV0eiIsImEiOiItMTJGWEF3In0.HEvuRMMVxBVR5-oDYvudxw'; 9 | 10 | // Create the map object with your mapId and token, 11 | // referencing the DOM element where you want the map to go. 12 | L.mapbox.accessToken = accessToken; 13 | var map = L.mapbox.map('map', mapId); 14 | 15 | // Set the initial view of the map to the whole US 16 | map.setView([39, -96], 4); 17 | 18 | // Great, now we have a basic web map! 19 | -------------------------------------------------------------------------------- /js/exercise_5.js: -------------------------------------------------------------------------------- 1 | 2 | var dataFileToAdd = 'data/restaurants.geojson'; 3 | 4 | var featureLayer = L.mapbox.featureLayer() 5 | .loadURL(dataFileToAdd) 6 | .addTo(map); 7 | 8 | featureLayer.on('ready', function() { 9 | this.eachLayer(function(layer){ 10 | layer.setIcon(L.mapbox.marker.icon({ 11 | 'marker-color': '#8834bb', 12 | 'marker-size': 'large', 13 | 'marker-symbol': 'restaurant' 14 | })) 15 | }); 16 | map.fitBounds(featureLayer.getBounds()); 17 | }); 18 | -------------------------------------------------------------------------------- /js/exercise_6.js: -------------------------------------------------------------------------------- 1 | //exercise 6 2 | featureLayer.on('ready', function(){ 3 | this.eachLayer(function(layer){ 4 | layer.bindPopup('Welcome to ' + layer.feature.properties.name); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /js/exercise_7.js: -------------------------------------------------------------------------------- 1 | // exercise 7 2 | var clickHandler = function(e){ 3 | $('#info').empty(); 4 | 5 | var feature = e.target.feature; 6 | 7 | $('#sidebar').fadeIn(400,function(){ 8 | var info = ''; 9 | 10 | info += '
    ' 11 | info += '

    ' + feature.properties.name + '

    ' 12 | if(feature.properties.phone) info += '

    ' + feature.properties.cuisine + '

    ' 13 | if(feature.properties.phone) info += '

    ' + feature.properties.phone + '

    ' 14 | if(feature.properties.phone) info += '

    ' + feature.properties.website + '

    ' 15 | info += '
    ' 16 | 17 | $('#info').append(info); 18 | }); 19 | }; 20 | 21 | featureLayer.on('ready', function(){ 22 | this.eachLayer(function(layer){ 23 | layer.on('click', clickHandler); 24 | }); 25 | }); 26 | 27 | map.on('click',function(e){ 28 | $('#sidebar').fadeOut(200); 29 | }); 30 | -------------------------------------------------------------------------------- /js/exercise_8.js: -------------------------------------------------------------------------------- 1 | // exercise 8 2 | var myLocation = L.mapbox.featureLayer().addTo(map); 3 | map.on('locationfound', function(e) { 4 | 5 | myLocation.setGeoJSON({ 6 | type: 'Feature', 7 | geometry: { 8 | type: 'Point', 9 | coordinates: [e.latlng.lng, e.latlng.lat] 10 | }, 11 | properties: { 12 | 'title': 'Here I am!', 13 | 'marker-color': '#ff8888', 14 | 'marker-symbol': 'star' 15 | } 16 | }); 17 | 18 | }); 19 | map.locate({setView:true}); 20 | -------------------------------------------------------------------------------- /js/save_shape_to_gist.js: -------------------------------------------------------------------------------- 1 | var shapeLayer; 2 | shapeLayer = L.geoJson().addTo(map); 3 | 4 | $('body').on('keyup',function(e){ 5 | if(e.ctrlKey && e.altKey && e.keyCode === 83){ 6 | enableShapfileTool(); 7 | } 8 | }); 9 | 10 | var mapframe = document.getElementById('map'); 11 | var dropbox = document.getElementById("drag-overlay"); 12 | mapframe.addEventListener("dragenter", dragenter, false); 13 | dropbox.addEventListener("dragenter", dropdragenter, false); 14 | dropbox.addEventListener("dragover", dragover, false); 15 | dropbox.addEventListener("drop", drop, false); 16 | dropbox.addEventListener("dragleave", function() { 17 | map.scrollWheelZoom.enable(); 18 | $('#drag-overlay').hide(); 19 | }, false); 20 | 21 | function dragenter(e) { 22 | e.stopPropagation(); 23 | e.preventDefault(); 24 | map.scrollWheelZoom.disable(); 25 | $('#drag-overlay').show(); 26 | } 27 | 28 | function dropdragenter(e) { 29 | e.stopPropagation(); 30 | e.preventDefault(); 31 | } 32 | 33 | function dragover(e) { 34 | e.stopPropagation(); 35 | e.preventDefault(); 36 | } 37 | 38 | function drop(e) { 39 | e.stopPropagation(); 40 | e.preventDefault(); 41 | map.scrollWheelZoom.enable(); 42 | $('#drag-overlay').hide(); 43 | var dt = e.dataTransfer; 44 | var files = dt.files; 45 | 46 | var i = 0; 47 | var len = files.length; 48 | 49 | if (!len) { 50 | return 51 | } 52 | while (i < len) { 53 | handleFile(files[i]); 54 | i++; 55 | } 56 | } 57 | 58 | function enableShapfileTool(){ 59 | 60 | var AddShapefileControl = L.Control.extend({ 61 | options: { 62 | position: 'bottomleft' 63 | }, 64 | 65 | onAdd: function(map) { 66 | this._container = L.DomUtil.create('div', 'nsi-control-query leaflet-bar leaflet-control'); 67 | this._input = L.DomUtil.create('input','hide-input',this._container); 68 | this._input.type = 'file'; 69 | this._input.id = 'input'; 70 | 71 | this._button = L.DomUtil.create('a','leaflet-bar-single',this._container); 72 | this._button.title = 'Add a shapefile to the map'; 73 | 74 | L.DomEvent 75 | .on(this._input, 'change', this._queryShapefile, this) 76 | .on(this._button, 'click', L.DomEvent.stopPropagation) 77 | .on(this._button, 'click', L.DomEvent.preventDefault) 78 | .on(this._button, 'click', this._fireInput, this); 79 | 80 | return this._container; 81 | }, 82 | 83 | _queryShapefile: function(){ 84 | var file = document.getElementById('input').files[0]; 85 | handleFile(file); 86 | }, 87 | _fireInput:function(){ 88 | this._input.click(); 89 | } 90 | }); 91 | 92 | var ShootShapefileControl = L.Control.extend({ 93 | options: { 94 | position: 'bottomleft' 95 | }, 96 | 97 | onAdd: function(map) { 98 | this._container = L.DomUtil.create('div', 'nsi-control-query-shape leaflet-bar leaflet-control'); 99 | this._button = L.DomUtil.create('a','leaflet-bar-single',this._container); 100 | this._button.title = 'Shoot your shapefile to a gist'; 101 | 102 | L.DomEvent 103 | .on(this._button, 'click', L.DomEvent.stopPropagation) 104 | .on(this._button, 'click', L.DomEvent.preventDefault) 105 | .on(this._button, 'click', this._fireData, this); 106 | 107 | return this._container; 108 | }, 109 | 110 | _fireData: function(){ 111 | // -> Shoot geoJSON to a github gist based on the code from the NSI application 112 | map.spin(true); 113 | $.ajax({ 114 | url:'https://api.github.com/gists', 115 | method:'POST', 116 | contentType:'application/json; charset=utf-8', 117 | dataType: 'json', 118 | data: JSON.stringify({ 119 | description: 'shapefile from the NSI tool', 120 | public: true, 121 | files: { 122 | 'file.geojson': { content: JSON.stringify(shapeLayer.toGeoJSON()) } 123 | } 124 | }), 125 | success:function(data){ 126 | map.spin(false); 127 | var url = data.html_url; 128 | if(confirm('the data has been transfered to a gist at this url: ' + url + ' would you like to go there now?')) window.open(url); 129 | } 130 | }); 131 | } 132 | }); 133 | 134 | map.addControl(new AddShapefileControl()); 135 | map.addControl(new ShootShapefileControl()); 136 | 137 | } 138 | 139 | function handleFile(file) { 140 | shapeLayer.clearLayers(); // remove any existing data -> comment out to allow multiple shapefiles 141 | 142 | map.spin(true); 143 | var reader = new FileReader(); 144 | 145 | if (file.name.slice(-3) === 'zip') { 146 | var reader = new FileReader(); 147 | reader.onload = function(){ 148 | if(reader.readyState !==2 || this.error){ 149 | return; 150 | }else{ 151 | shp(reader.result).then(function(geoJson){ 152 | 153 | map.spin(false); 154 | shapeLayer.addData(geoJson,{ 155 | filter:function(feature,layer){ 156 | if(feature.geometry.coordinates[0].length>1000){ 157 | return true; 158 | } 159 | } 160 | }); 161 | 162 | window.setTimeout(function(){ 163 | map.fitBounds(shapeLayer.getBounds()) 164 | },50); 165 | 166 | },function(e){ 167 | map.spin(false); 168 | console.log('Error: ', e); 169 | }); 170 | } 171 | }; 172 | reader.readAsArrayBuffer(file); 173 | }else{ 174 | return undefined; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/decode.js: -------------------------------------------------------------------------------- 1 | // This is adapted from the implementation in Project-OSRM 2 | // https://github.com/DennisOSRM/Project-OSRM-Web/blob/master/WebContent/routing/OSRM.RoutingGeometry.js 3 | window.polyline = {}; 4 | window.polyline.decode = function(str, precision) { 5 | var index = 0, 6 | lat = 0, 7 | lng = 0, 8 | coordinates = [], 9 | shift = 0, 10 | result = 0, 11 | byte = null, 12 | latitude_change, 13 | longitude_change, 14 | factor = Math.pow(10, precision || 6); 15 | 16 | // Coordinates have variable length when encoded, so just keep 17 | // track of whether we've hit the end of the string. In each 18 | // loop iteration, a single coordinate is decoded. 19 | while (index < str.length) { 20 | 21 | // Reset shift, result, and byte 22 | byte = null; 23 | shift = 0; 24 | result = 0; 25 | 26 | do { 27 | byte = str.charCodeAt(index++) - 63; 28 | result |= (byte & 0x1f) << shift; 29 | shift += 5; 30 | } while (byte >= 0x20); 31 | 32 | latitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1)); 33 | 34 | shift = result = 0; 35 | 36 | do { 37 | byte = str.charCodeAt(index++) - 63; 38 | result |= (byte & 0x1f) << shift; 39 | shift += 5; 40 | } while (byte >= 0x20); 41 | 42 | longitude_change = ((result & 1) ? ~(result >> 1) : (result >> 1)); 43 | 44 | lat += latitude_change; 45 | lng += longitude_change; 46 | 47 | coordinates.push([lng / factor, lat / factor]); 48 | } 49 | 50 | return coordinates; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/spin.js: -------------------------------------------------------------------------------- 1 | 2 | L.SpinMapMixin = { 3 | spin: function (state, options) { 4 | if (!!state) { 5 | // start spinning ! 6 | if (!this._spinner) { 7 | this._spinner = new Spinner(options).spin(this._container); 8 | this._spinning = 0; 9 | } 10 | this._spinning++; 11 | } 12 | else { 13 | this._spinning--; 14 | if (this._spinning <= 0) { 15 | // end spinning ! 16 | if (this._spinner) { 17 | this._spinner.stop(); 18 | this._spinner = null; 19 | } 20 | } 21 | } 22 | } 23 | }; 24 | 25 | L.Map.include(L.SpinMapMixin); 26 | 27 | L.Map.addInitHook(function () { 28 | this.on('layeradd', function (e) { 29 | // If added layer is currently loading, spin ! 30 | if (e.layer.loading) this.spin(true); 31 | if (typeof e.layer.on !== 'function') return; 32 | e.layer.on('data:loading', function () { this.spin(true); }, this); 33 | e.layer.on('data:loaded', function () { this.spin(false); }, this); 34 | }, this); 35 | this.on('layerremove', function (e) { 36 | // Clean-up 37 | if (e.layer.loading) this.spin(false); 38 | if (typeof e.layer.on !== 'function') return; 39 | e.layer.off('data:loaded'); 40 | e.layer.off('data:loading'); 41 | }, this); 42 | }); 43 | 44 | //fgnass.github.com/spin.js#v2.0.1 45 | !function(a,b){"object"==typeof exports?module.exports=b():"function"==typeof define&&define.amd?define(b):a.Spinner=b()}(this,function(){"use strict";function a(a,b){var c,d=document.createElement(a||"div");for(c in b)d[c]=b[c];return d}function b(a){for(var b=1,c=arguments.length;c>b;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return l[e]||(m.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",m.cssRules.length),l[e]=1),e}function d(a,b){var c,d,e=a.style;for(b=b.charAt(0).toUpperCase()+b.slice(1),d=0;d',c)}m.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.width,left:d.radius,top:-d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.length+d.width,k=2*j,l=2*-(d.width+d.length)+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k