├── README.md ├── module-01 ├── app │ ├── data │ │ ├── mapstogpx20161028_220258.gpx │ │ └── route.js │ └── index.html ├── leaflet-map-template │ └── index.html ├── lesson-01.md └── lesson-images │ ├── data-drawn.png │ ├── dev-tools-elements.gif │ ├── draw-geojson.gif │ ├── file-structure-brackets.png │ ├── geojson-io-add-point.gif │ ├── geojson-io-edit.gif │ ├── geojson-styled.png │ ├── google-maps-route.png │ ├── inspect-elements.gif │ ├── leaflet-template.png │ ├── mapstogpx.png │ ├── open-developer-tools.png │ └── ui-tooltip.gif ├── module-02 ├── app │ ├── data │ │ ├── flight.xls │ │ ├── ghg-co-2015.csv │ │ ├── ghg-co-2015.json │ │ ├── ghg-emissions.json │ │ └── metadata.txt │ ├── index-solution.html │ └── index.html ├── lesson-02.md └── lesson-images │ ├── csv-export-options.png │ ├── csv-geojson-io.png │ ├── error-404.png │ ├── exploring-data-structure.gif │ ├── file-cleaned.png │ ├── file-openoffice.png │ ├── flight.png │ ├── ghg-quantities-logged.png │ ├── hover-affordance.gif │ ├── leaflet-geojson-api.png │ ├── log-feature-properties.gif │ ├── log-features-hover.gif │ ├── path-options.png │ ├── points-as-markers.png │ ├── radius-40.png │ ├── save-csv.png │ ├── stack-trace.png │ ├── styled-circles.png │ └── styled-circles2.png ├── module-03 ├── app │ ├── data │ │ ├── 2016CountyHealthRankings.csv │ │ └── counties.json │ └── index.html └── lesson-03.md └── module-04 ├── README.md ├── marker-cluster ├── data │ └── stations.geojson └── index.html └── turf-hex ├── data └── accidents.csv └── index.html /README.md: -------------------------------------------------------------------------------- 1 | # Open Source Cartography: A Web Mapping Short Course 2 | 3 | The availability of code-free, push-button interfaces for making sophisticated web maps remains an ongoing goal for the web mapping community (e.g., [CARTO's Builder](https://carto.com/builder/), [Mapbox's Studio](https://www.mapbox.com/mapbox-studio/), or [ESRI's Configurable Apps](http://www.esri.com/software/configurable-apps)). However, proficiency in the web scripting languages of HTML, SVG, CSS, and JavaScript allows web cartographers to enhance the user experience of web maps beyond what is provided by such tools. The growing availability of freely distributed, open source code libraries and open Application Programming Interfaces (APIs) provide web mappers with advanced geoprocessing, representation, and interaction capabilities. This short course will introduce students to the design and development practices of building customized web maps and explore a variety of thematic mapping techniques. 4 | 5 | **Warning:** This repo was built for a series of hands-on workshops taught at CU-Boulder in the Fall of 2016 and not intended to be fully standalone for online learning. The Module 03 write-up still needs to be completed as well. Have fun! 6 | 7 | ## Learning Objectives 8 | 9 | By the end of this course, students will be able to: 10 | 11 | * Identify and construct the principle technological components of a web mapping development stack. 12 | * Assemble geographic data into appropriate data formats for integration within a web mapping system. 13 | * Demonstrate the ability to load data from local and remote sources and draw geometry features within a client-side web browser. 14 | * Interpret the functionality provided by a web mapping library's Application Programming Interface (API) and adapt examples to specific use cases. 15 | * Construct functional thematic web maps using raster, point, line, and areal symbology. 16 | * Experiment with various plugins designed to extend the core functionality of a web mapping library. 17 | * Utilize a remote web server for hosting and sharing web maps. 18 | 19 | ## Requirements 20 | 21 | ### Software Requirements 22 | 23 | The course will make use of the following software components, which are freely available on the web: 24 | 25 | * A text editor ([Brackets](http://brackets.io/), [Sublime](https://www.sublimetext.com/), or [Atom](https://atom.io/)) 26 | * A modern web browser (Chrome or Firefox recommended) 27 | * Web developer tools (built into modern browsers) 28 | * A local web server 29 | 30 | The course will introduce and demonstrate use cases for the following open source JavaScript libraries: 31 | 32 | * [JQuery](https://jquery.com/) 33 | * [Leaflet](http://leafletjs.com/) 34 | * [Turf](http://turfjs.org/) 35 | * [Simple Statistics](http://simplestatistics.org/) 36 | * [TopoJson](https://github.com/mbostock/topojson/wiki) 37 | 38 | Additionally, course materials are made available through the [GitHub](https://github.com) web platform. These may be downloaded as a zip file or cloned through a git command or desktop client interface. GitHub is also used to freely host and serve complete web maps over the Internet. 39 | 40 | ### Assumed Background Knowledge 41 | 42 | The learning curve for web development is steep, and mastering web mapping requires wrestling with an intimidating array of tools and technical jargon. Teaching computer programming and covering the breadth of geospatial technologies is beyond the scope of this short course. Some experience with computer programming or web design is helpful, and supplementary technical learning is encouraged. 43 | 44 | That stated, the course is intended as a gentle introduction to web mapping and assumes no expertise in coding or Geographic Information Science. Modules provide code examples and templates for completion of exercises. 45 | 46 | Most importantly, the course requires curiosity and a willingness to be confused and frustrated while puzzling through problems. 47 | 48 | ## Additional Help and Resources 49 | 50 | * [Stack Overflow](http://stackoverflow.com/) 51 | * [GIS Stack Exchange](http://gis.stackexchange.com/) 52 | * [Leaflet Documentation](http://leafletjs.com/) 53 | 54 | ## Weekly Module Summary 55 | 56 | ### 1. Drawing Data with Web Maps 57 | 58 | The introductory lesson acquaints students with the key components of a web mapping technology stack and the development environment. Students will learn how to use Leaflet, a popular and mobile-friendly web mapping library, to build a simple web map using data converted to GeoJSON format. The class will explore techniques for drawing (or representing) geographic features such as points and lines on various available basemaps, styling these features, and adding basic user interaction to retreive specific information about meaningful places on the map. 59 | 60 | ### 2. Thematic Web Mapping: Point Symbology 61 | 62 | The second lesson explores cartographic techniques for representing point features within a web map. Students learn a process for loading Comma Separated Values (CSV) data into a script and converting them to GeoJSON before drawing them to the map. Students are taught a technique for programmatically re-sizing circles based on quantitative data attribute values to create a proportional symbol map. Students then extend the point symbol map to encode nominal attribute data using color to produce a bi-variate map. The lesson concludes by introducing user interaction techniques for filtering represented data. 63 | 64 | ### 3. Thematic Web Mapping: Value by Area 65 | 66 | The third lesson explores techniques for making the popular choropleth map. Students use web-based tools first to convert polygons such as census tracks into appropriate data formats before writing code to dynamically classify data for representing different values. The lesson also introduces the popular JavaScript library Qjuery, to assist in loading data from external sources. 67 | 68 | ### 4. Extending Web Maps with Plugins and Web Hosting 69 | 70 | The final module further extends the core functionality of a library's API through the employment of [plugins](http://leafletjs.com/plugins.html), which build upon the open codebase of the Leaflet library. Students are encouraged to play with plugin functionality best-suited to meet the map user's objectives. Finally, the module documents a streamlined approach for easily hosting web maps using GitHub. -------------------------------------------------------------------------------- /module-01/app/data/mapstogpx20161028_220258.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sverrir Sigmundarson 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 2770 Winding Trail Drive 17 | 2770 Winding Trail Drive, Boulder, CO 80304, USA 18 | 19 | 20 | Guggenheim Geography 21 | Guggenheim Geography, Boulder, CO 80302, USA 22 | 23 | 24 | 2770 Winding Trail Drive to Guggenheim Geography 25 | 1 26 | 27 | 28 | TP001 29 | 30 | 31 | TP002 32 | 33 | 34 | TP003 35 | 36 | 37 | TP004 38 | 39 | 40 | TP005 41 | 42 | 43 | TP006 44 | 45 | 46 | TP007 47 | 48 | 49 | TP008 50 | 51 | 52 | TP009 53 | 54 | 55 | TP010 56 | 57 | 58 | TP011 59 | 60 | 61 | TP012 62 | 63 | 64 | TP013 65 | 66 | 67 | TP014 68 | 69 | 70 | TP015 71 | 72 | 73 | TP016 74 | 75 | 76 | TP017 77 | 78 | 79 | TP018 80 | 81 | 82 | TP019 83 | 84 | 85 | TP020 86 | 87 | 88 | TP021 89 | 90 | 91 | TP022 92 | 93 | 94 | TP023 95 | 96 | 97 | TP024 98 | 99 | 100 | TP025 101 | 102 | 103 | TP026 104 | 105 | 106 | TP027 107 | 108 | 109 | TP028 110 | 111 | 112 | TP029 113 | 114 | 115 | TP030 116 | 117 | 118 | TP031 119 | 120 | 121 | TP032 122 | 123 | 124 | TP033 125 | 126 | 127 | TP034 128 | 129 | 130 | TP035 131 | 132 | 133 | TP036 134 | 135 | 136 | TP037 137 | 138 | 139 | TP038 140 | 141 | 142 | TP039 143 | 144 | 145 | TP040 146 | 147 | 148 | TP041 149 | 150 | 151 | TP042 152 | 153 | 154 | TP043 155 | 156 | 157 | TP044 158 | 159 | 160 | TP045 161 | 162 | 163 | TP046 164 | 165 | 166 | TP047 167 | 168 | 169 | TP048 170 | 171 | 172 | TP049 173 | 174 | 175 | TP050 176 | 177 | 178 | TP051 179 | 180 | 181 | TP052 182 | 183 | 184 | TP053 185 | 186 | 187 | TP054 188 | 189 | 190 | TP055 191 | 192 | 193 | TP056 194 | 195 | 196 | TP057 197 | 198 | 199 | TP058 200 | 201 | 202 | TP059 203 | 204 | 205 | TP060 206 | 207 | 208 | TP061 209 | 210 | 211 | TP062 212 | 213 | 214 | TP063 215 | 216 | 217 | TP064 218 | 219 | 220 | TP065 221 | 222 | 223 | TP066 224 | 225 | 226 | TP067 227 | 228 | 229 | TP068 230 | 231 | 232 | TP069 233 | 234 | 235 | TP070 236 | 237 | 238 | TP071 239 | 240 | 241 | TP072 242 | 243 | 244 | TP073 245 | 246 | 247 | TP074 248 | 249 | 250 | TP075 251 | 252 | 253 | TP076 254 | 255 | 256 | TP077 257 | 258 | 259 | TP078 260 | 261 | 262 | TP079 263 | 264 | 265 | TP080 266 | 267 | 268 | TP081 269 | 270 | 271 | TP082 272 | 273 | 274 | TP083 275 | 276 | 277 | TP084 278 | 279 | 280 | TP085 281 | 282 | 283 | TP086 284 | 285 | 286 | TP087 287 | 288 | 289 | TP088 290 | 291 | 292 | TP089 293 | 294 | 295 | TP090 296 | 297 | 298 | TP091 299 | 300 | 301 | TP092 302 | 303 | 304 | TP093 305 | 306 | 307 | TP094 308 | 309 | 310 | TP095 311 | 312 | 313 | TP096 314 | 315 | 316 | TP097 317 | 318 | 319 | TP098 320 | 321 | 322 | TP099 323 | 324 | 325 | TP100 326 | 327 | 328 | TP101 329 | 330 | 331 | TP102 332 | 333 | 334 | TP103 335 | 336 | 337 | TP104 338 | 339 | 340 | TP105 341 | 342 | 343 | TP106 344 | 345 | 346 | TP107 347 | 348 | 349 | TP108 350 | 351 | 352 | TP109 353 | 354 | 355 | TP110 356 | 357 | 358 | TP111 359 | 360 | 361 | TP112 362 | 363 | 364 | TP113 365 | 366 | 367 | TP114 368 | 369 | 370 | TP115 371 | 372 | 373 | TP116 374 | 375 | 376 | TP117 377 | 378 | 379 | TP118 380 | 381 | 382 | TP119 383 | 384 | 385 | TP120 386 | 387 | 388 | TP121 389 | 390 | 391 | TP122 392 | 393 | 394 | TP123 395 | 396 | 397 | TP124 398 | 399 | 400 | TP125 401 | 402 | 403 | TP126 404 | 405 | 406 | TP127 407 | 408 | 409 | TP128 410 | 411 | 412 | TP129 413 | 414 | 415 | TP130 416 | 417 | 418 | TP131 419 | 420 | 421 | TP132 422 | 423 | 424 | TP133 425 | 426 | 427 | TP134 428 | 429 | 430 | TP135 431 | 432 | 433 | TP136 434 | 435 | 436 | TP137 437 | 438 | 439 | TP138 440 | 441 | 442 | TP139 443 | 444 | 445 | TP140 446 | 447 | 448 | TP141 449 | 450 | 451 | TP142 452 | 453 | 454 | TP143 455 | 456 | 457 | TP144 458 | 459 | 460 | TP145 461 | 462 | 463 | TP146 464 | 465 | 466 | TP147 467 | 468 | 469 | TP148 470 | 471 | 472 | TP149 473 | 474 | 475 | TP150 476 | 477 | 478 | TP151 479 | 480 | 481 | TP152 482 | 483 | 484 | TP153 485 | 486 | 487 | TP154 488 | 489 | 490 | TP155 491 | 492 | 493 | TP156 494 | 495 | 496 | TP157 497 | 498 | 499 | TP158 500 | 501 | 502 | TP159 503 | 504 | 505 | TP160 506 | 507 | 508 | TP161 509 | 510 | 511 | TP162 512 | 513 | 514 | TP163 515 | 516 | 517 | TP164 518 | 519 | 520 | TP165 521 | 522 | 523 | TP166 524 | 525 | 526 | TP167 527 | 528 | 529 | TP168 530 | 531 | 532 | TP169 533 | 534 | 535 | TP170 536 | 537 | 538 | TP171 539 | 540 | 541 | TP172 542 | 543 | 544 | TP173 545 | 546 | 547 | TP174 548 | 549 | 550 | TP175 551 | 552 | 553 | TP176 554 | 555 | 556 | TP177 557 | 558 | 559 | TP178 560 | 561 | 562 | TP179 563 | 564 | 565 | TP180 566 | 567 | 568 | TP181 569 | 570 | 571 | TP182 572 | 573 | 574 | TP183 575 | 576 | 577 | TP184 578 | 579 | 580 | TP185 581 | 582 | 583 | TP186 584 | 585 | 586 | TP187 587 | 588 | 589 | TP188 590 | 591 | 592 | TP189 593 | 594 | 595 | TP190 596 | 597 | 598 | TP191 599 | 600 | 601 | TP192 602 | 603 | 604 | TP193 605 | 606 | 607 | TP194 608 | 609 | 610 | TP195 611 | 612 | 613 | TP196 614 | 615 | 616 | TP197 617 | 618 | 619 | TP198 620 | 621 | 622 | TP199 623 | 624 | 625 | TP200 626 | 627 | 628 | TP201 629 | 630 | 631 | TP202 632 | 633 | 634 | TP203 635 | 636 | 637 | TP204 638 | 639 | 640 | TP205 641 | 642 | 643 | TP206 644 | 645 | 646 | TP207 647 | 648 | 649 | TP208 650 | 651 | 652 | TP209 653 | 654 | 655 | TP210 656 | 657 | 658 | TP211 659 | 660 | 661 | TP212 662 | 663 | 664 | TP213 665 | 666 | 667 | TP214 668 | 669 | 670 | TP215 671 | 672 | 673 | TP216 674 | 675 | 676 | TP217 677 | 678 | 679 | TP218 680 | 681 | 682 | TP219 683 | 684 | 685 | TP220 686 | 687 | 688 | TP221 689 | 690 | 691 | TP222 692 | 693 | 694 | TP223 695 | 696 | 697 | TP224 698 | 699 | 700 | TP225 701 | 702 | 703 | TP226 704 | 705 | 706 | TP227 707 | 708 | 709 | TP228 710 | 711 | 712 | TP229 713 | 714 | 715 | TP230 716 | 717 | 718 | TP231 719 | 720 | 721 | TP232 722 | 723 | 724 | TP233 725 | 726 | 727 | TP234 728 | 729 | 730 | TP235 731 | 732 | 733 | TP236 734 | 735 | 736 | TP237 737 | 738 | 739 | TP238 740 | 741 | 742 | TP239 743 | 744 | 745 | TP240 746 | 747 | 748 | TP241 749 | 750 | 751 | TP242 752 | 753 | 754 | TP243 755 | 756 | 757 | TP244 758 | 759 | 760 | TP245 761 | 762 | 763 | TP246 764 | 765 | 766 | TP247 767 | 768 | 769 | TP248 770 | 771 | 772 | TP249 773 | 774 | 775 | TP250 776 | 777 | 778 | TP251 779 | 780 | 781 | TP252 782 | 783 | 784 | TP253 785 | 786 | 787 | TP254 788 | 789 | 790 | TP255 791 | 792 | 793 | TP256 794 | 795 | 796 | TP257 797 | 798 | 799 | TP258 800 | 801 | 802 | TP259 803 | 804 | 805 | TP260 806 | 807 | 808 | TP261 809 | 810 | 811 | TP262 812 | 813 | 814 | TP263 815 | 816 | 817 | TP264 818 | 819 | 820 | TP265 821 | 822 | 823 | TP266 824 | 825 | 826 | TP267 827 | 828 | 829 | 830 | -------------------------------------------------------------------------------- /module-01/app/data/route.js: -------------------------------------------------------------------------------- 1 | var data = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"2770 Winding Trail Drive to Guggenheim Geography"},"geometry":{"type":"LineString","coordinates":[[-105.2594084,40.0436406],[-105.25948,40.04364],[-105.25962,40.04363],[-105.25985,40.04355],[-105.2598539,40.043547],[-105.25992,40.04359],[-105.26001,40.04367],[-105.26004,40.04371],[-105.26017,40.04389],[-105.26021,40.04394],[-105.26026,40.04398],[-105.26046,40.04408],[-105.26079,40.04424],[-105.26087,40.0443],[-105.26093,40.04436],[-105.26094,40.0444],[-105.26095,40.04446],[-105.26094,40.04452],[-105.26092,40.04464],[-105.26091,40.04476],[-105.26091,40.04486],[-105.26092,40.04498],[-105.26094,40.04505],[-105.26096,40.04514],[-105.26099,40.04518],[-105.26102,40.04522],[-105.26124,40.04542],[-105.26133,40.04553],[-105.26136,40.04555],[-105.2614,40.04556],[-105.2615,40.04558],[-105.26166,40.0456],[-105.26194,40.0456],[-105.26262,40.04562],[-105.26287,40.04562],[-105.26312,40.04565],[-105.26319,40.04566],[-105.26327,40.04567],[-105.2633,40.04568],[-105.26335,40.04565],[-105.263347,40.0456533],[-105.26329,40.04556],[-105.26327,40.0455],[-105.26326,40.04542],[-105.26326,40.0451],[-105.26325,40.04381],[-105.26325,40.04374],[-105.26325,40.0434],[-105.26326,40.04322],[-105.26327,40.04312],[-105.26328,40.04288],[-105.26328,40.04256],[-105.26327,40.04227],[-105.26326,40.04191],[-105.26325,40.0415],[-105.26325,40.04099],[-105.26325,40.04067],[-105.26324,40.04044],[-105.26323,40.04038],[-105.26321,40.04033],[-105.26318,40.04029],[-105.26311,40.04022],[-105.2631127,40.0402176],[-105.26317,40.04015],[-105.26321,40.04009],[-105.26324,40.04002],[-105.26325,40.03992],[-105.26325,40.03936],[-105.26324,40.039],[-105.26324,40.03863],[-105.26324,40.03836],[-105.26323,40.03799],[-105.26323,40.03786],[-105.26323,40.03776],[-105.26323,40.03736],[-105.26321,40.03648],[-105.2632128,40.0364827],[-105.26322,40.03639],[-105.26323,40.03596],[-105.26323,40.0356],[-105.26322,40.03535],[-105.26321,40.03484],[-105.26323,40.03459],[-105.2632,40.03386],[-105.2632,40.03316],[-105.2632,40.03283],[-105.26319,40.0324],[-105.26318,40.03114],[-105.26317,40.0304],[-105.26316,40.03014],[-105.26314,40.02946],[-105.26313,40.02917],[-105.26312,40.0282],[-105.26311,40.02807],[-105.26312,40.02792],[-105.26312,40.02789],[-105.26312,40.02776],[-105.26312,40.02757],[-105.26311,40.02749],[-105.2631,40.02742],[-105.26306,40.02723],[-105.26301,40.02698],[-105.263,40.02691],[-105.26299,40.02677],[-105.26299,40.02668],[-105.26299,40.02655],[-105.26298,40.02615],[-105.26298,40.02591],[-105.26299,40.02585],[-105.26302,40.02574],[-105.2631,40.02553],[-105.26314,40.02545],[-105.26325,40.02529],[-105.26343,40.02508],[-105.26354,40.02492],[-105.26364,40.02473],[-105.26371,40.02454],[-105.26374,40.02443],[-105.26375,40.02438],[-105.26379,40.0242],[-105.2638,40.02412],[-105.26378,40.02394],[-105.26375,40.02375],[-105.26364,40.02342],[-105.26356,40.02317],[-105.26342,40.02275],[-105.26343,40.02269],[-105.26343,40.02268],[-105.2634,40.02242],[-105.26337,40.02224],[-105.26331,40.02184],[-105.26324,40.02137],[-105.26323,40.02129],[-105.26317,40.02033],[-105.26316,40.01997],[-105.26311,40.01987],[-105.26311,40.01965],[-105.26311,40.01876],[-105.26315,40.01865],[-105.26316,40.01794],[-105.26316,40.01746],[-105.26316,40.0174],[-105.26316,40.01734],[-105.26315,40.01678],[-105.26314,40.0164],[-105.26314,40.01625],[-105.26314,40.01587],[-105.26315,40.01565],[-105.26317,40.01544],[-105.26317,40.01533],[-105.26318,40.01512],[-105.26317,40.01469],[-105.26319,40.01459],[-105.26314,40.01373],[-105.26315,40.01356],[-105.26315,40.01335],[-105.26315,40.01328],[-105.26314,40.01327],[-105.26313,40.01325],[-105.2631,40.01322],[-105.26308,40.01311],[-105.26308,40.01279],[-105.26309,40.01261],[-105.2631,40.01244],[-105.26311,40.01234],[-105.26312,40.01223],[-105.26315,40.01207],[-105.2632,40.01186],[-105.26321,40.0118],[-105.26326,40.01161],[-105.26329,40.01151],[-105.26329,40.0115],[-105.26332,40.01142],[-105.26335,40.01134],[-105.26339,40.01123],[-105.26344,40.01114],[-105.26348,40.01106],[-105.26353,40.011],[-105.26357,40.01096],[-105.26372,40.01073],[-105.2638,40.01062],[-105.26384,40.01058],[-105.26392,40.01046],[-105.26399,40.01033],[-105.26405,40.0102],[-105.26411,40.01001],[-105.26412,40.00997],[-105.26412,40.00993],[-105.26413,40.00992],[-105.26413,40.00987],[-105.26413,40.00982],[-105.26413,40.00956],[-105.26415,40.009],[-105.26414,40.00872],[-105.26414,40.00823],[-105.2641373,40.0082273],[-105.26484,40.00824],[-105.26519,40.00825],[-105.2651907,40.0082478],[-105.26519,40.00815],[-105.26517,40.00776],[-105.2651749,40.0077592],[-105.2653,40.00776],[-105.26543,40.00775],[-105.2658,40.00775],[-105.26617,40.00775],[-105.26627,40.00775],[-105.26637,40.00774],[-105.26652,40.00774],[-105.26676,40.00774],[-105.26704,40.00774],[-105.2670375,40.0077384],[-105.26704,40.00729],[-105.2670377,40.0072925],[-105.26733,40.00729],[-105.26783,40.00729],[-105.26809,40.00729],[-105.26818,40.00729],[-105.2681777,40.0072878],[-105.26818,40.00736],[-105.26818,40.00749],[-105.26818,40.00751],[-105.26818,40.00752],[-105.26818,40.00754],[-105.26819,40.00754],[-105.26821,40.00755],[-105.26837,40.00755],[-105.2684,40.00756],[-105.26842,40.00757],[-105.26842,40.0076],[-105.26842,40.00781],[-105.26861,40.00785],[-105.2688,40.00789],[-105.26892,40.00794],[-105.26904,40.008],[-105.26918,40.00805],[-105.26928,40.00811],[-105.26942,40.00819],[-105.26963,40.00829],[-105.2696325,40.0082893],[-105.26968,40.00826],[-105.26971,40.00825],[-105.26975,40.00824],[-105.27061,40.00824],[-105.2707,40.00824],[-105.27093,40.00823],[-105.27128,40.00822],[-105.2712823,40.0082233],[-105.27129,40.0084],[-105.2712892,40.0084025],[-105.27184,40.0084],[-105.27232,40.0084],[-105.27276,40.0084],[-105.2727592,40.0084045],[-105.2731,40.00803],[-105.27311,40.00802],[-105.27312,40.008],[-105.27312,40.00798],[-105.27313,40.00797],[-105.27313,40.00785],[-105.2731258,40.0078514],[-105.27327,40.00785],[-105.27378,40.00786],[-105.27388,40.00787],[-105.27395,40.00787],[-105.2743,40.00802],[-105.2743014,40.0080197]]}},{"type":"Feature","properties":{"name":"My Home"},"geometry":{"type":"Point","coordinates":[-105.2594094,40.0435913]}},{"type":"Feature","properties":{"name":"Guggenheim Geography"},"geometry":{"type":"Point","coordinates":[-105.2742872,40.0081661]}},{"type":"Feature","properties":{"name":"Frank's Hot Dog Stand"},"geometry":{"type":"Point","coordinates":[-105.26563167572021,40.021616902296195]}}]} -------------------------------------------------------------------------------- /module-01/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Leaflet Map Template 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 77 | 78 | 79 | 80 | 81 |

Rich's time in Boulder

82 | 83 |
84 | 85 |
86 |

about this map

87 | 88 |

Additional information about the data and map goes here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis urna magna, maximus nec laoreet sit amet, dictum ultricies nibh. Ut id auctor lacus. Nam a dolor et justo luctus luctus.

89 | 90 |

Duis a elit eget risus dictum vehicula id eu elit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae. Sed sed enim nisl. 91 |

92 | 93 |

Map authored by me, MY NAME

94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /module-01/leaflet-map-template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Leaflet Map Template 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 77 | 78 | 79 | 80 | 81 |

This is the title to my awesome map.

82 | 83 |
84 | 85 |
86 |

about this map

87 | 88 |

Additional information about the data and map goes here. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis urna magna, maximus nec laoreet sit amet, dictum ultricies nibh. Ut id auctor lacus. Nam a dolor et justo luctus luctus.

89 | 90 |

Duis a elit eget risus dictum vehicula id eu elit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae. Sed sed enim nisl. 91 |

92 | 93 |

Map authored by me, MY NAME

94 | 95 |
96 | 97 | 98 | 99 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /module-01/lesson-01.md: -------------------------------------------------------------------------------- 1 | # Lesson 01. Drawing Data with Web Maps 2 | 3 | To begin, clone or [download the course repository](https://github.com/rgdonohue/web-mapping-short-course) to your computer. 4 | 5 | Clone or unzip the contents of the files in a known location (i.e., a directory in your Documents). Briefly, examine the contents of the directory named *module-01/*. You notice a directory named *leaflet-map-template/* that contains an HTML document named *index.html*. 6 | 7 | ## Setting up a development environment 8 | 9 | There are many [lessons and resources](http://maptime.io/lessons-resources/) available on the web for getting started with web mapping. But often, getting the development environment set up so you can write code and test your map is the first roadblock. Let's consider the principle components of a web development process. These are what you need to have available every time you begin web map design and development. 10 | 11 | **A Web Browser** 12 | 13 | Web Mapping is an unusual form of Cartography in that we primarily make the maps using the same medium with which we develop them. The web browser is a crucial component of our technology stack. Most web map developers use Chrome or Firefox, as should you. Make sure you have installed recent updates to your browser, and begin by opening your Web Browser. 14 | 15 | **A Text Editor** 16 | 17 | Developers build web maps by writing plain text. Text editors designed for web development facilitate this, particular by highlighting different parts of the code syntax. Install and open one of the following: 18 | 19 | * [Brackets](http://brackets.io/) 20 | * [Sublime](https://www.sublimetext.com/) 21 | * [Atom](https://atom.io/) 22 | 23 | Brackets is particularly handy if you don't have a local server running or don't know what that is. Its "Live Preview" functionality runs a local test server within your web browser, which allows you to see and test your rendered web application. 24 | 25 | **Directories, Files, Data, and Media** 26 | 27 | The user's web browser assembles and renders the web page and map application using specific files. Minimally this will include an HTML index file for the direction, typically named *index.html*. 28 | 29 | Using your text editor, open the *module-01/* directory so you can view the entire structure. A good text editor allows you view and modify the contents of the directory. Again examine the contents of this directory from within the editor. 30 | 31 | ![Viewing the directory file structure and files in Brackets](lesson-images/file-structure-brackets.png) 32 | **Figure 01.** Viewing the directory file structure and files in Brackets. 33 | 34 | 35 | **A Web Server** 36 | 37 | While a web browser application interprets and renders the files that compose our web maps, it doesn't do this by itself. It requires a "server" to gather the files and deliver them to the browser correctly. A server is especially necessary when using JavaScript to make what are known as [asynchronous requests](http://rowanmanning.com/posts/javascript-for-beginners-async/) to load files and data into our web application after the web page initially loads. 38 | 39 | We develop a web application "locally" on our computer, so it's best to use a local server, or a "local test server," to do this. There are a few options for getting a local test server running on your machine. 40 | 41 | 1. [Python's SimpleHTTPServer](http://www.pythonforbeginners.com/modules-in-python/how-to-use-simplehttpserver/) module 42 | 2. A [WAMP server](http://www.wampserver.com/en/) for Windows or a [Mac OS alternative](http://ampps.com/download) 43 | 3. The use of [Brackets](http://brackets.io/) text editor (see above) 44 | 45 | Once you get a local server running within your browser, open the *leaflet-map-template/index.html* file (or open this file from within Brackets using the Live Preview). Note that Brackets will launch a new instance of your default web browser application. 46 | 47 | **Web Developer Tools** 48 | 49 | Modern web browsers come installed with web developer tools. These tools come loaded with functionality allowing you to investigate how a web page or application is structured and performing within your browser. Read more about using the [Chrome DevTools](https://developer.chrome.com/devtools), and as always look for the shortcuts to open and close the toolbar in your browser (Cmd + i in Mac OS). 50 | 51 | Open your web developer toolbar. 52 | 53 | ![Opening Chrome's Web Developer Tools](lesson-images/open-developer-tools.png) 54 | **Figure 02.** Opening Chrome's Web Developer Tools. 55 | 56 | You can use the Elements tab of the Developer tool to inspect the DOM as it is rendered within the browser. While this will largely mirror the HTML document itself, the rendered DOM will also contain elements dynamically produced with JavaScript when the page loads. 57 | 58 | ![Exploring the DOM elements using Chrome's Web Developer Tools](lesson-images/dev-tools-elements.gif) 59 | **Figure 03.** Exploring the DOM elements using Chrome's Web Developer Tools. 60 | 61 | One of the most useful features is the [Console](https://developer.mozilla.org/en-US/docs/Web/API/Console), which allows you to log JavaScript values within the browser. You can type directly into the Console, or log values from a JavaScript file loaded within the browser. We'll be doing both as we build and debug web maps. 62 | 63 | 64 | ## Introducing the building blocks of a "hello world" web map template 65 | 66 | Let's begin with a simple working template for making a web map. 67 | 68 | Open the *leaflet-map-template/index.html* file within your browser. Be sure to open your file using the Live Preview of Brackets or using another local server. 69 | 70 | If the map loads correctly, you should see a light basemap centered on the Guggenheim Geography building within Boulder, CO. A marker is located at this location and a tooltip opens on the marker. There should also be no errors in the Developer Tool Console. 71 | 72 | ![The Leaflet map templete loaded in the browser](lesson-images/leaflet-template.png) 73 | **Figure 04.** The Leaflet map templete loaded in the browser, with no errors in the Console. 74 | 75 | How does the code contained within the *index.html* file work together to produce this map? Our web document is composed of 3 essential web technologies: HTML, CSS, and JavaScript. The web browser in turn creates a DOM or Document Object Module using this document. 76 | 77 | 78 | ### HTML (structure) 79 | 80 | The HTML structures and describes the content of our document. Some of its elements (` 100 | 101 | 102 | 103 | 104 |

This is the title to my awesome map.

105 | 106 | 107 | 108 | 109 | 114 | 115 | 116 | 117 | 118 | ``` 119 | 120 | Read more about [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML). 121 | 122 | ### CSS (form) 123 | 124 | If the HTML structures our content, the primarly role of CSS is to give that structure form. We apply CSS rules to the web page and elements we draw to the map to adjust the "look and feel" of the page and map. 125 | 126 | ```css 127 | body { 128 | margin: 0; 129 | padding: 0; 130 | background: "whitesmoke"; 131 | font-family: "Noto Sans", sans-serif; 132 | color: #3d3d3d; 133 | } 134 | 135 | h1 { 136 | position: absolute; 137 | margin-top: 0; 138 | top: 10px; 139 | left: 45px; 140 | font-size: 2em; 141 | font-family: "Lora", serif; 142 | letter-spacing: .04em; 143 | padding: 10px 15px; 144 | background: rgba(256, 256, 256, .4); 145 | border: 1px solid grey; 146 | border-radius: 3px; 147 | z-index: 800; 148 | } 149 | 150 | #map { 151 | position: absolute; 152 | top: 0; 153 | bottom: 0; 154 | width: 100%; 155 | } 156 | 157 | ``` 158 | 159 | Read more about [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS). 160 | 161 | ### JavaScript (behavior) 162 | 163 | Finally, we use JavaScript to add event-driven and interaction behavior to our website; to make it dynamic! The following JavaScript creates the map within our page. 164 | 165 | 166 | ```javascript 167 | 168 | // options to be used when creating the map 169 | var options = { 170 | center: [40.00816, -105.27423], 171 | zoom: 12 172 | } 173 | 174 | // creation of the Leaflet map 175 | var map = L.map('map', options); 176 | 177 | // request to load basemap slippy tiles 178 | var tiles = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', { 179 | attribution: '© OpenStreetMap © CartoDB', 180 | subdomains: 'abcd', 181 | maxZoom: 19 182 | }).addTo(map); 183 | 184 | // string content to be inserted into a tooltip 185 | var message = 'Guggenheim Geography!'; 186 | 187 | // create a Leaflet marker, centered on the map's center 188 | L.marker(map.getCenter()) 189 | .bindTooltip(message) // bind the tooltip and message to the marker 190 | .addTo(map) // add the marker to the map 191 | .openTooltip(); // open the tooltip 192 | ``` 193 | 194 | Let's quickly review what the JavaScript above is doing: 195 | 196 | 1. The statement assigning an object to the variable `options` is specifying some of [Leaflet's map object's options](http://leafletjs.com/reference-1.0.0.html#map-option), here centering the map on Guggenheim and setting a zoom level to 12. 197 | 198 | 2. The next statement creates a [Leaflet map object](http://leafletjs.com/reference-1.0.0.html#map), inserts it into a DOM element with the `id` value of `map` (i.e., our `
` within our HTML, and applies the options we specified. 199 | 200 | 3. The statement assigning a value to the variable `tiles` is a fun one. Leaflet uses this one to request a set of basemap tiles from a remote server. There are lots of these, and you can have fun swapping them out for one another until you find one appropriate for your map. Check out the options at [Leaflet Providers](https://leaflet-extras.github.io/leaflet-providers/preview/). 201 | 202 | 4. The final two statements assign a string value `'Guggenheim Geography!'` to the variable `message` and uses the Leaflet [L.Marker class](http://leafletjs.com/reference-1.0.0.html#marker) to place a marker at the center of the map, as well as the [Leaflet L.Tooltip class](http://leafletjs.com/reference-1.0.0.html#tooltip) to display our message on top of the map layers. 203 | 204 | The [Leaflet API Reference](http://leafletjs.com/reference-1.0.0.html) is your source for understanding how the Leaflet JavaScript operates. 205 | 206 | Read more about [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript). 207 | 208 | Let's now use this template to create a basic thematic map. 209 | 210 | ## Mapping scenario: mapping your route from home to campus 211 | 212 | For this lesson, we're going to make a Leaflet map of your route to or from campus (pick one for now if they are different). The goal of the map is to allow the user to see your starting and ending points, the route between them, as well as a couple places of interest along the way. Maybe you stop at a cafe or your place of work on the way to class. Perhaps you frequent a watering hole on the way home. 213 | 214 | We want to capture this geography, convert it to an appropriate data format, and display it on a web and mobile-friendly map. Additionally, we will allow the user to retrieve specific information about these places through interacting with the map (in this case, hovering over the map or touching on a marker). 215 | 216 | To begin, make a copy of the *leaflet-map-template/* directory and rename it to *app/*, within the *module-01/* directory. 217 | 218 | ### Step 1: Data aquisition and conversion 219 | 220 | To get our data and convert it to an appropriate format for web mapping, we're going to use a few web-based tools. 221 | 222 | Let's first use a fantastic (proprietary) mapping resource: [Google Maps](https://www.google.com/maps)! 223 | 224 | 1. First, search for your primary campus building. For instance, I'll do a simple Google Map search for [Guggenheim Geography](https://www.google.com/maps/place/Guggenheim+Geography,+Boulder,+CO+80302/@40.0081521,-105.2764582,16.98z/data=!4m5!3m4!1s0x876bec31173b714d:0xfe71b5ee5f7fee43!8m2!3d40.0081661!4d-105.2742872). 225 | 226 | 2. Then use the Directions functionality to determine the route from your home to your building. **BE CAREFUL:** This map is going to end up on the web. Do you want people knowing your exact address? Maybe not! Use an approximate address location instead. Also, be sure to select the mode of travel. Do you drive a car, ride a bus, walk, or ride your bike? If you fly a plane to get to campus, that's incredible! 227 | 228 | ![Using Google Maps to find a route and mode from home to Campus](lesson-images/google-maps-route.png) 229 | **Figure 05.** Using Google Maps to find a route and mode from home to Campus. 230 | 231 | Google Maps allows you to pick between alternative routes, as well as to drag the route to customize the route you really take. Feel free to adjust this a little bit, but don't worry too much about it. We'll be using another tool to do this later on. 232 | 233 | When you have highlighted your desired route in blue on the map, copy the entire URL from the address bar (highlight it, and select *Edit -> Copy* or *Cntr + C*). 234 | 235 | 3. Next, go to a website named [Maps To GPX](https://mapstogpx.com/), a tool that, "accepts a link to pre-made Google Directions and converts them to a GPX file." This is perfect for us! 236 | 237 | Paste your URL from Google Maps into the form and hit "Let's Go." 238 | 239 | ![Converting Google Maps Route to GPX](lesson-images/mapstogpx.png) 240 | **Figure 06.** Converting Google Maps Route to GPX. 241 | 242 | The site will make the necessary conversion and prompt the GPX file to download (with a name something like *mapstogpx20161026_000913.gpx*). Move this file into the *module-01/app/data/* directory. 243 | 244 | GPX (the GPS Exchange Format) is a text-based format derived from XML and often used to encode GPS data. You can open this file in your text editor to examine the contents. We're not too interested in particular file, but it looks like this: 245 | 246 | ```xml 247 | 248 | 249 | 250 | 251 | Sverrir Sigmundarson 252 | 253 | 254 | 255 | 256 | 257 | 258 | 3315 13th Street 259 | 3315 13th Street, Boulder, CO 80304, USA 260 | 261 | ``` 262 | 263 | If you happen to use the popular [Strava](https://www.strava.com/) service, you can also download all your routes in GPX format. 264 | 265 | 4. Next, we want to convert our data to another format: [GeoJSON](http://geojson.org/). GeoJSON is to web mapping what the Shapefile is to GIS. 266 | 267 | Navigate your browser to a website called [geojson.io](http://geojson.io/). You'll want to bookmark this website, as it's an extremely useful online tool. 268 | 269 | Open your GPX file in the geojson.io web application. Study the code generated in the right-hand panel; it is a valid GeoJSON encoding of your route. Unlike Shapefiles, GeoJSON can encode multiple geometry types within a single Feature Collection. Note that Features have both `properties` and `geometry` attributes. The `"LineString"` type contains all the points that make up the route, while the two `"Point"` type Features encode the endpoints of the route. 270 | 271 | Take some time to play around with the geojson.io website and your data. 272 | 273 | Note that our process has retained some data attributes from Google Maps that we don't need. We can remove these, and edit the existing data properties as we wish. The web application also allows us to add, remove, and edit geometries. For example, you could modify your route. 274 | 275 | ![Removing unneeded attribute properties in geojson.io](lesson-images/geojson-io-edit.gif) 276 | **Figure 07.** Removing unneeded attribute properties in geojson.io. 277 | 278 | Instead, let's add a couple more places of interest. Using the drawing tools, place a point of interest along your route. Add a property row to the marker, and be sure to use the word "name" as the name of the attribute (just like the other points). Geojson.io also adds some other properties to style the marker. We don't need these, and you can remove them in the editor. 279 | 280 | ![Adding a placemarker in geojson.io](lesson-images/geojson-io-add-point.gif) 281 | **Figure 08.** Adding a placemarker in geojson.io. 282 | 283 | Once you finish editing your data, choose **Save** and download as a GeoJSON (it will download with the file name *map.geojson*. Save or move this downloaded file into your *module-01/app/data/* directory. 284 | 285 | You can open this file in your text editor to see that it's the same as what geojson.io displayed. 286 | 287 | ```js 288 | {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"Northbrook Drive to Guggenheim Geography"},"geometry":{"type":"LineString","coordinates":[[-105.2601844,40.0446618],[-105.26024,40.04472],[-105.2602362,40.0447162],[-105.26029,40.04469],[-105.26033,40.04466],[-105.26043,40.04458],[-105.26056,40.04451],[-105.26065,40.04448],[-105.2607,40.04447],[-105.26073,40.04447],[-105.26075,40.04446],[-105.26078,40.04443],[-105.2608,40.04439],[-105.26083,40.04438],[-105.26086,40.04437],[-105.26093,40.04436],[-105.2609275,40.0443637],[-105.26094,40.0444],[-105.26095,40.04446] 289 | ``` 290 | 291 | This last step of modifying our data attributes, editing the geometries, and exporting to GeoJSON wraps up our data acquisition and conversion process. Next, let's get the data loaded into the web map. 292 | 293 | ### Step 2: Loading external data into a web document. 294 | 295 | There are various ways to load GeoJSON data into your web map. While the best (and more sophisticated) way is to make an [AJAX](https://en.wikipedia.org/wiki/Ajax) request, we'll begin with a more simple solution. 296 | 297 | First, rename the *map.geojson* file to *route.js*. Then open the *route.js* file in your text editor and assign the entire GeoJSON structure to a variable named *data*: 298 | 299 | ```js 300 | var data = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"name":"Northbrook Drive to Guggenheim Geography"},"geometry":{"type":"LineString","coordinates":[[-105.2601844,40.0446618],[-105.26024,40.04472],[-105.2602362,40.0447162],[-105.26029,40.04469],[-105.26033,40.04466],[-105.26043,40.04458],[-105.26056,40.04451],[-105.26065,40.04448],[-105.2607,40.04447],[-105.26073,40.04447],[-105.26075,40.04446],[-105.26078,40.04443],[-105.2608,40.04439],[-105.26083,40.04438],[-105.26086,40.04437],[-105.26093,40.04436],[-105.2609275,40.0443637],[-105.26094,40.0444],[-105.26095,40.04446] 301 | ``` 302 | 303 | Save those changes to the file. GeoJSON is essentially a JavaScript object, and we've simply assigned it to a JavaScript variable. The variable `data` will now be available to us within our script. 304 | 305 | Next open the *index.html* file within our *module-01/app/* directory. 306 | 307 | The script is currently loading the external using the `` to our *index.html* file, beneath where we load the external JavaScript files but (importantly) BEFORE the `` tags enclosing our custom JavaScript. 310 | 311 | ```javascript 312 | 313 | 314 | 315 | // OUR DATA LOADED HERE! 316 | 317 | 132 | 133 | 296 | 297 | 298 | 299 | 300 | -------------------------------------------------------------------------------- /module-02/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Green House Gas Emissions 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 113 | 114 | 115 | 116 | 117 |

It's getting hot out west!

118 | 119 |
120 | 121 |
122 |

2015 Green House Gas Emissions in Colorado

123 | 124 |

This proportional symbol map shows the relative magnitude of green house gas emissions in 2015 by large facilites. Data 125 | are shown by 126 | fuel combustion and 127 | process emissions. 128 | 129 |

Metric tons of GHG

130 |
131 |
132 |
133 |
134 |
135 |
136 |

The data are reported to the EPA and available from their 137 | FLIGHT web applicaiton.

138 |

Map authored 139 | Rich Donohue.

140 | 141 |
142 | 143 | 144 | 145 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /module-02/lesson-02.md: -------------------------------------------------------------------------------- 1 | # Lesson 02. Thematic Web Mapping: Point Symbology 2 | 3 | This lesson guides you through a process of creating a proportional symbol map. We're going to work with some greenhouse gas emissions data to make a map looking something like this: [http://situatedlaboratories.net/carbon-emissions-co/app/](http://situatedlaboratories.net/carbon-emissions-co/app/). 4 | 5 | Proportional symbol maps are a useful alternative to the common choropleth map. We can use them to map total or ratio data and unlike choropleth maps we don't need to standardize the data. We can use precise point data to make a proportional symbol map (such as the location of a coal plant), or conceptual point data (i.e., a wind farm covers a large area, but we can still represent it as a point). Proportional symbol maps are also good at showing relative magnitudes (i.e., "I can tell that this one is larger than that one"). We can make proportional symbol maps with any shape, though circles are the most common. 6 | 7 | To begin, clone or [download the course repository](https://github.com/rgdonohue/web-mapping-short-course) to your computer. 8 | 9 | Clone or unzip the contents of the files in a known location (i.e., a directory in your Documents). Examine the contents of the directory named *module-02/*. You notice a directory named *app/* that contains an HTML document named *index.html*. 10 | 11 | Before proceeding, verify you have correctly set up your development environment with the necessary components. Refer to [Lesson 01: Setting up a development environment](https://github.com/rgdonohue/web-mapping-short-course/blob/master/module-01/lesson-01.md#setting-up-a-development-environment) for help. 12 | 13 | 14 | ## Step 01: Data wrangling and conversion 15 | 16 | First, let's get some data!. We're first going to download some tabular data from the web and manipulate it using a spreadsheet program. We'll then save it as a CSV file format before converting it to GeoJSON. 17 | 18 | ### Getting the data 19 | 20 | 21 | Navigate to the EPA's [Facility Level Information on GreenHouse Gasses Tool (FLIGHT)](https://ghgdata.epa.gov/ghgp/main.do). The tool offers you several filtering options. 22 | 23 | ![EPA's FLIGHT app filtered for 2015 data in Colorado](lesson-images/flight.png) 24 | **Figure 01.** EPA's FLIGHT app filtered for 2015 data in Colorado. 25 | 26 | Narrow your search to a specific US state (e.g., Colorado) for 2015 data, and leave the rest of the options as default. When ready, click **Export Data** to download the data file. Note that we download it as a Microsoft Xcel file named *flight.xls*. Save or move this file into the *data/* directory within the *module-02/app/* directory. 27 | 28 | ### Cleaning the data and converting to CSV 29 | 30 | Next, open this file within a spreadsheet program. You can do this using Microsoft Xcel, but note the word of caution below regarding character encoding. We need to be very careful with these next steps. I'll be demonstrating this using OpenOffice. 31 | 32 | Currently, the data are encoded within the *.xls* file format (which OpenOffice and LibreOffice can read). Our next step is to clean this data up a bit and convert it to a CSV file. Because we're working with point-level data where each feature has a latitude and longitude value, the CSV will be the most efficient flat-file format in which to store the data. 33 | 34 | ![The flight.xls file opened in OpenOffice](lesson-images/file-openoffice.png) 35 | **Figure 02.** The flight.xls file opened in OpenOffice. 36 | 37 | We first note that there are metadata stored within the first few rows of the file. You may want to copy this information and paste it into a *metadata.txt* file within your *data/* directory. Then delete the first five rows from the sheet. 38 | 39 | Next, we can examine the data and remove any columns we don't need. Remember that when serving maps over the web we want to keep file sizes as small as we can. Even though the broadband internet is fast, we're also designing our maps to work on mobile devices and a broader global audience with potentially limited bandwidth. 40 | 41 | Remove the columns with headings "REPORTING YEAR", "PARENT COMPANIES", and "SUBPARTS". 42 | 43 | Next, we want to further prepare the data for use within a scripting environment. **Spaces can be problematic in the names of files, directories, and data attributes. DON'T USE SPACES!!!** I had to yell that one because it is important and perpetually violated. 44 | 45 | Let's run through the column headings (i.e., our data attributes) and edit them to remove spaces and make them more useful for our script later on. 46 | 47 | Make the following changes: 48 | 49 | * Change "FACILITY NAME" to "NAME" 50 | * Change "GHGRP ID" to "GHGRP_ID" (note how I use an underscore here) 51 | * Change "REPORTED ADDRESS" to "ADDRESS" 52 | * Change "CITY NAME" to "CITY" 53 | * Change "COUNTY NAME NAME" to "COUNTY" 54 | * Change "ZIP CODE" to "ZIP" 55 | * Change "GHG QUANTITY (METRIC TONS CO2e)" to "GHG_QUANTITY" 56 | 57 | Save the changes to the *.xls* file. 58 | 59 | ![The flight.xls file cleaned up in OpenOffice](lesson-images/file-cleaned.png) 60 | **Figure 03.** The flight.xls file cleaned up in OpenOffice. 61 | 62 | Next, we want to convert this file to a CSV format. While this should be a simple "Save As .." we need to be careful with this step. Different programs use different character encodings to save a CSV file, as well as different line break characters. This can cause problems later on when we load and parse the data using JavaScript in the browser. 63 | 64 | When choosing **File -> Save As ...** in OpenOffice, I can choose the File type to be "Text CSV (.csv). I can also rename the file to something useful like *ghg-co-2015.csv*. 65 | 66 | ![Saving the file as a CSV in OpenOffice](lesson-images/save-csv.png) 67 | **Figure 04.** Saving the file as a CSV in OpenOffice. 68 | 69 | Importantly, open office gives us an additional dialog box with **Export Text File** options. Ensure that the "Character set" is "Unicode (UTF-8)", the field delimiter is a comma, and that you've checked the box to "Quote all text cells." 70 | 71 | ![Export Text File options in OpenOffice](lesson-images/csv-export-options.png) 72 | **Figure 05.** Export Text File options in OpenOffice. 73 | 74 | Click OK to export the CSV file, saving it again within the *module-02/app/data/* directory. 75 | 76 | **Protip:** The open source text editor [Text Wrangler](http://www.barebones.com/products/TextWrangler/) is a great tool to have in your arsenal (NotePad++ is functionally equivalent for PCs). While not the developer's editor of choice, Text Wrangler handles large files well, is good for doing quick Find and Replace operations, and is helpful in ensuring your CSV file is saved with the correct character encoding, as well as how the line breaks are encoded (Legacy Mac OS (CR), Unix (LF), or Windows (CRLF)). LF or CR usually cause the least amount of problems. 77 | 78 | ### Converting the CSV data to GeoJSON 79 | 80 | There are many ways to convert a CSV file to GeoJSON. One of the easiest (surprise, surprise!) is to use the web-based tool [geojson.io](http://geojson.io/). 81 | 82 | Open the file in the web interface. Note that geojson.io has converted the attribute column headings and values within the CSV to key/value pairs within a `properties` object for each point feature in our data. 83 | 84 | ![The CSV file converted to GeoJSON in geojson.io](lesson-images/csv-geojson-io.png) 85 | **Figure 06.** The CSV file converted to GeoJSON in geojson.io. 86 | 87 | Save this file to your local machine as a GeoJSON, and rename the file from *map.geojson* to *ghg-co-2015.json*. 88 | 89 | Note how in this example, the original .xls file heading "Name" was edited to contain a space before the "N" ... " NAME". This is not a trivial error in coding and may cause problems later when trying to programmatically access the value of each feature's NAME attribute. Also, unlike the CSV in which that one heading entry can easily be edited, when we convert it to GeoJSON that mistake is replicated within the properties of each feature. However, a find and replace in a powerful text editor can quickly rectify the problem. 90 | 91 | 92 | ## Step 02: Loading data into the script 93 | 94 | Begin by opening the *module-02/app/index.html* in your text editor and load the page in your browser using a local server (see [Lesson 01](https://github.com/rgdonohue/web-mapping-short-course/blob/master/module-01/lesson-01.md)). 95 | 96 | We can see that a basic Leaflet map template has loaded within the page (using CARTO's light basemap tiles). With the developer tools open, we see there is also a 404 (Not Found) error generated within the script. 97 | 98 | ![Basic Leaflet map template loaded with 404 error](lesson-images/error-404.png) 99 | **Figure 07.** Basic Leaflet map template loaded with 404 error. 100 | 101 | Within the toolbar, we can drill down into what's known as the "call stack" or "stack trace" to further diagnose the error and where it occurs within our script. We see that within our *index.html* file the error is thrown on line 129. 102 | 103 | ![Examining the call stack error](lesson-images/stack-trace.png) 104 | **Figure 08.** Examining the call stack error. 105 | 106 | The error is thrown within the JavaScript that is attempting to use an asynchronous request to load an external file. 107 | 108 | ```javascript 109 | $.getJSON('data/data.json', function (data) { 110 | 111 | console.log(data); 112 | 113 | }); 114 | ``` 115 | 116 | The problem here is that JQuery's `.getJSON()` method is looking for a data file that doesn't exist. We don't currently have a file at this location: `'data/data.json'`. 117 | 118 | Edit the JavaScript to look for our file named ghg-co-2015.json, save the changes and refresh the browser. Provided there were no errors in the CSV encoding or any additional errors in the JavaScript, you should see our point data drawn as the default Leaflet markers on the map. 119 | 120 | ![GeoJSON data drawn as point markers](lesson-images/points-as-markers.png) 121 | **Figure 09.** GeoJSON data drawn as point markers. 122 | 123 | Note that we are logging to Console the variable `data`, which refers to our GeoJSON data we've loaded into the script. You can "drill down" to explore the structure of the data within the Console. It's the same structure that geojson.io helped us produce. 124 | 125 | ![Exploring the data logged to the Console](lesson-images/exploring-data-structure.gif) 126 | **Figure 10.** Exploring the data logged to the Console. 127 | 128 | The `index.html` starter file has quite a bit of JavaScript already written for us, with several key statements commented out. Let's take a moment to get the big picture of how this script is structured before exploring some of the details of how we turn these markers into proportional symbols. 129 | 130 | You'll note that the script is broken down into many functions. Once our data is loaded, we call a function named `drawMap` and send our `data` variable as an argument. Within the `drawMap` function we currently use the Leaflet [L.GeoJson() method](http://leafletjs.com/reference-1.0.0.html#geojson) to convert this to a Leaflet object and add it to our map. 131 | 132 | ```javascript 133 | function drawMap(data) { 134 | 135 | var options = { 136 | 137 | pointToLayer: pointToLayer, 138 | style: style, 139 | onEachFeature: onEachFeature 140 | 141 | } 142 | 143 | L.geoJson(data).addTo(map); 144 | 145 | // L.geoJson(data, options).addTo(map); 146 | 147 | // drawLegend(data) 148 | 149 | } 150 | ``` 151 | 152 | Before we do anything else, note the `options` object within this function: 153 | 154 | ```javascript 155 | var options = { 156 | 157 | pointToLayer: pointToLayer, 158 | style: style, 159 | onEachFeature: onEachFeature 160 | 161 | } 162 | ``` 163 | 164 | These are options we can use when creating a Leaflet L.geoJson object, and the [API reference](http://leafletjs.com/reference-1.0.0.html#geojson) documents what they do and how to use them. 165 | 166 | ![The Leaflet API Reference options for L.GeoJson](lesson-images/leaflet-geojson-api.png) 167 | **Figure 11.** The Leaflet API Reference options for L.GeoJson. 168 | 169 | The key on the left is associated with a function on the right of each, as you scan the code below, you'll see that there are three respective functions for each of these options (here I've named the functions the same as the key, but these functions could be named anything). 170 | 171 | When we pass this `options` object to the method that creates the Leaflet L.GeoJson() object, these functions will eventually do the following: 172 | 173 | * The `pointToLayer` function will convert our point-level feature data to a Leaflet "layer" such as a vector layer. Specifically, we're going to convert them into [Leaflet's CircleMarker](http://leafletjs.com/reference-1.0.0.html#circlemarker) vector layer. Features become "layers" in Leaflet. 174 | * The `style` function will return a style object used to change the visual appearance of the vector layers Leaflet draws to the map. All vector layers inherit the options available to the [L.Path class](http://leafletjs.com/reference-1.0.0.html#path) (see Figure 12 below). You can edit, add, and remove these options. 175 | * The `onEachFeature` function will iterate over all our features as they are converted to vector layers. This function allows us to do things like attach event handlers, such as a `'mouseover'` event to trigger a visual affordance or a tooltip. 176 | 177 | ![The Leaflet API Reference for Path options](lesson-images/path-options.png) 178 | **Figure 12.** The Leaflet API Reference for Path options. 179 | 180 | These functions are all called when the `options` object used when creating the Leaflet GeoJson layergroup (note in the API Reference that there are other methods to update the layers after creation. 181 | 182 | 183 | ## Step 03: Drawing the point data as styled circles 184 | 185 | Within the `drawMap` function body, comment out the line that is drawing our data to the map as markers, and uncomment the line that is passing the `options` object to the L.geoJson() method. 186 | 187 | ```javascript 188 | // L.geoJson(data).addTo(map); 189 | 190 | L.geoJson(data, options).addTo(map); 191 | ``` 192 | 193 | Save these changes and refresh the browser. You'll now see our markers have been replaced by circleMarkers, styled with the options we're currently designating with the `style` function. 194 | 195 | ![The Leaflet circleMarkers](lesson-images/styled-circles.png) 196 | **Figure 13.** The Leaflet circleMarkers. 197 | 198 | Let's first fiddle with some of the code within two of the functions being called, the `pointToLayer` function and the `style` function. 199 | 200 | Within the `pointToLayer` function we're returning a Leaflet circleMarker object, and passing one styling rule within this, setting the radius to 10 pixels. 201 | 202 | Try changing this numeric value to something like 40. Saving and refreshing reveal how we can programmatically adjust the radius size of the circles. 203 | 204 | ```javascript 205 | function pointToLayer(feature, latlng) { 206 | 207 | // console.log(feature.properties); 208 | 209 | return L.circleMarker(latlng, { 210 | 211 | radius: 40 212 | 213 | }); 214 | 215 | } 216 | ``` 217 | 218 | ![The Leaflet circleMarkers styled with a radius of 40](lesson-images/radius-40.png) 219 | **Figure 14.** The Leaflet circleMarkers styled with a radius of 40. 220 | 221 | Next, try adjusting some of the style options within the `style` function. For instance, we could make the stroke width larger (the `weight` property) and change its color value from `'whitesmoke'` to a hexadecimal value: 222 | 223 | 224 | ```javascript 225 | function style(feature) { 226 | 227 | var styleOptions = { 228 | 229 | fillColor: '#bd4932', 230 | fillOpacity: 1, 231 | color: "#105b63", 232 | weight: 6 233 | 234 | } 235 | 236 | return styleOptions; 237 | 238 | } 239 | 240 | ``` 241 | 242 | The result is again apparent in on the map. 243 | 244 | ![The Leaflet circleMarkers styled with different stroke color and heavier stroke weight](lesson-images/styled-circles2.png) 245 | **Figure 15.** The Leaflet circleMarkers styled with different stroke color and heavier stroke weight. 246 | 247 | Now that we have a basic handle on styling let's return to the radius of our circles and figure out how to not size them uniformly, but given a quantitative value. 248 | 249 | 250 | ## Step 04: Scaling the circles to a known quantity 251 | 252 | If we think back to our data, we remember that we encoded the amount of greenhouse gasses emitted by different facilities with an attribute of `GHG_QUANTITY` (we can also look in our GeoJSON data file for reference). 253 | 254 | Within the `pointToLayer` function, first, uncomment the line logging `feature.properties` to the Console. 255 | 256 | 257 | ```javascript 258 | function pointToLayer(feature, latlng) { 259 | 260 | console.log(feature.properties); 261 | 262 | return L.circleMarker(latlng, { 263 | 264 | radius: 40 265 | 266 | }); 267 | 268 | } 269 | ``` 270 | 271 | Saving and refreshing in the browser allow you to see each feature's properties logged to the Console. 272 | 273 | ![Inspecting the properites of each feature logged to Console](lesson-images/log-feature-properties.gif) 274 | **Figure 16.** Inspecting the properites of each feature logged to Console. 275 | 276 | This last example is a really important aspect of web map development. Within the script, we need to know how to access the data (in this case the `properties` of each GeoJSON point feature) associated or bound to features. 277 | 278 | Modify the statement to include the `GHG_QUANTITY` property name: `console.log(feature.properties.GHG_QUANTITY);`. 279 | 280 | We can now see how we can access the specific value we're interested in as the code loops through all the features in our data. 281 | 282 | ![Logging a specific properity value of each feature to Console](lesson-images/ghg-quantities-logged.png) 283 | **Figure 17.** Logging a specific properity value of each feature to Console. 284 | 285 | Now we can simply replace the hard-coded value for our radius with the code accessing the value of each. 286 | 287 | ```javascript 288 | function pointToLayer(feature, latlng) { 289 | 290 | 291 | return L.circleMarker(latlng, { 292 | 293 | radius: feature.properties.GHG_QUANTITY 294 | 295 | }); 296 | 297 | } 298 | ``` 299 | 300 | However, there are currently two problems with this. 1.) These numbers are quite large, and our circles are drawn with diameters exceeding the extent of the map. 2.) We've used the data values to directly scale the symbol's radii. However, when making visual comparisons as it is currently coded, our eye-brain systems don't compare the radii; rather we compare the areas of these circles. Thus, a plant producing 20 times more emissions than another doesn't appear to have 20 times the area here, but an area much higher than that (20 squared times pi!). 301 | 302 | To make the map work with human brains and eyes, we calculate radii to create these areas and use these to draw the circles. 303 | 304 | To implement this in our code, instead of directly assigning a value to the radius of each CircleMarker, let's assign the return value of a function, which will scale and calculate the radius for us. This is as simple as calling a function and passing our data value to it: `radius: feature.properties.GHG_QUANTITY`. 305 | 306 | Below in the script we see I've already written a function to do the circle area calculation and scaling. This function accepts a value we send to it, does the calculation, and returns a value. Note that you may need to fiddle with the value of the `scaleFactor` a bit given a particular dataset. 307 | 308 | ```javascript 309 | function calcRadius(val) { 310 | 311 | var radius = Math.sqrt(val / Math.PI), 312 | scaleFactor = .04; 313 | 314 | return radius * scaleFactor; 315 | 316 | } 317 | ``` 318 | 319 | Once we made these modifications to our script, we should have a proportionally-scaled set of circles representing the relative magnitudes of gas emissions. 320 | 321 | There may be one little problem, however, often associated with proportional symbol maps. The large circles may overlap or completely cover smaller circles. Fortunately, we can add make two modifications to our current script to help solve this problem. 322 | 323 | First, we can apply a sorting method to our data before we draw our circles, sorting the features with the largest quantities of greenhouse gas first and therefore drawing them first. The smaller ones will be stacked on top. Add this code within the function loading our data, before we call the function `drawMap`: 324 | 325 | ```javascript 326 | data.features.sort(function (a, b) { 327 | return b.properties.GHG_QUANTITY - a.properties.GHG_QUANTITY 328 | }); 329 | ``` 330 | 331 | Next, adjust the `fillOpacity` property of the circles (within our `style` function) to something like `.6`. 332 | 333 | ## Step 05: Adding user interaction affordances and retrieving information 334 | 335 | Next, we want to add some interactivity for the user. To do so, we'll make use of the `onEachFeature` function, which like the others will iterate over all our data features when they are drawn to the map. We want to achieve two things: 1.) give the user a visual affordance that they've hovered over the symbol, and 2.) allow the user to retrieve some specific info about the plant, displayed in a Leaflet tooltip. 336 | 337 | Within the `onEachFeature` function, we're attaching what's known as an "event listener" to each layer. We're actually attaching two listeners, one for when the user mouses over a layer and another for when the user mouses off a layer. First, uncomment the statement within the function. Like the statement above, this will log to console the feature properties of the target layer when the user mouses over it. 338 | 339 | ```javascript 340 | function onEachFeature(feature, layer) { 341 | 342 | layer.on({ 343 | mouseover: function () { 344 | 345 | console.log(feature.properties); 346 | 347 | }, 348 | mouseout: function () { 349 | 350 | 351 | } 352 | }) 353 | 354 | } 355 | ``` 356 | 357 | We can inspect the output in the developer toolbar. 358 | 359 | ![Logging a specific properity value of each feature to Console on hover](lesson-images/log-features-hover.gif) 360 | **Figure 18.** Logging a specific properity value of each feature to Console on hover. 361 | 362 | Next, modify the code within the `onEachFeature` function to change the stroke color of the target layer. Note that we have access to both the original GeoJSON feature data here, as well as the vector layer being created. We want to modify the style rules of the vector layer. 363 | 364 | ```javascript 365 | layer.on({ 366 | mouseover: function () { 367 | 368 | layer.setStyle({ 369 | color: "yellow", 370 | weight: 3 371 | }); 372 | 373 | }, 374 | mouseout: function () { 375 | 376 | layer.setStyle({ 377 | color: "#105b63", 378 | weight: 1 379 | }); 380 | } 381 | }); 382 | ``` 383 | 384 | This code is using a Leaflet `setStyle` method to change the values of two style properties, the stroke color, and width. On the mouseout event, we want to set them back to their original look. 385 | 386 | ![Offering the user a visual affordance on mouseover](lesson-images/hover-affordance.gif) 387 | **Figure 19.** Offering the user a visual affordance on mouseover. 388 | 389 | Finally, we want the user to retrieve some specific information about each feature on mouseover. While we could build a more sophisticated info window or display this information in a side panel, for now, a simple tooltip will work for us. 390 | 391 | Add the following code to the `onEachFeature` function: 392 | 393 | ```javascript 394 | function onEachFeature(feature, layer) { 395 | 396 | layer.on({ 397 | mouseover: function () { 398 | 399 | layer.setStyle({ 400 | color: "yellow", 401 | weight: 3 402 | }); 403 | 404 | }, 405 | mouseout: function () { 406 | 407 | layer.setStyle({ 408 | color: "#105b63", 409 | weight: 1 410 | }); 411 | } 412 | }); 413 | 414 | 415 | var toolTipInfo = "Name: " + feature.properties.NAME + "
" + 416 | "C02: " + feature.properties.GHG_QUANTITY.toLocaleString() + "metric tons" 417 | 418 | 419 | layer.bindTooltip(toolTipInfo, { sticky: true }); 420 | 421 | } 422 | ``` 423 | 424 | Note that we're again accessing specific information from each feature using the `feature.properties` syntax. We're also using a convenient JavaScript method `toLocaleString()` to format the number nicely with commas in the tooltip. 425 | 426 | ## Making the map whole 427 | 428 | So far we've coded a fairly slick web map. But it's currently lacking some crucical elements that make the map more useful for the user. For one, maps should have a catchy if not informative title. You can also display supplementary information in a sidebar or overlay. Crucially, however, most thematic maps need a legend. We want the user to easily know what they're looking at, without having to deduce it or poke around too much. 429 | 430 | Drawing dynamic legends in web mapping can be tricky business, however. One solution is to use HTML and CSS to draw the basic form of the legend, and then let the JavaScript update and position the legend elements appropriately. See the example in this [bi-variate map of facilities emissions](https://github.com/rgdonohue/carbon-emissions-co/blob/master/app/index.html). 431 | 432 | -------------------------------------------------------------------------------- /module-02/lesson-images/csv-export-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/csv-export-options.png -------------------------------------------------------------------------------- /module-02/lesson-images/csv-geojson-io.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/csv-geojson-io.png -------------------------------------------------------------------------------- /module-02/lesson-images/error-404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/error-404.png -------------------------------------------------------------------------------- /module-02/lesson-images/exploring-data-structure.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/exploring-data-structure.gif -------------------------------------------------------------------------------- /module-02/lesson-images/file-cleaned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/file-cleaned.png -------------------------------------------------------------------------------- /module-02/lesson-images/file-openoffice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/file-openoffice.png -------------------------------------------------------------------------------- /module-02/lesson-images/flight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/flight.png -------------------------------------------------------------------------------- /module-02/lesson-images/ghg-quantities-logged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/ghg-quantities-logged.png -------------------------------------------------------------------------------- /module-02/lesson-images/hover-affordance.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/hover-affordance.gif -------------------------------------------------------------------------------- /module-02/lesson-images/leaflet-geojson-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/leaflet-geojson-api.png -------------------------------------------------------------------------------- /module-02/lesson-images/log-feature-properties.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/log-feature-properties.gif -------------------------------------------------------------------------------- /module-02/lesson-images/log-features-hover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/log-features-hover.gif -------------------------------------------------------------------------------- /module-02/lesson-images/path-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/path-options.png -------------------------------------------------------------------------------- /module-02/lesson-images/points-as-markers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/points-as-markers.png -------------------------------------------------------------------------------- /module-02/lesson-images/radius-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/radius-40.png -------------------------------------------------------------------------------- /module-02/lesson-images/save-csv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/save-csv.png -------------------------------------------------------------------------------- /module-02/lesson-images/stack-trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/stack-trace.png -------------------------------------------------------------------------------- /module-02/lesson-images/styled-circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/styled-circles.png -------------------------------------------------------------------------------- /module-02/lesson-images/styled-circles2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgdonohue/web-mapping-short-course/0ce336497e89518e8690df7f227dbe4b50951195/module-02/lesson-images/styled-circles2.png -------------------------------------------------------------------------------- /module-03/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | US County Health Rankings 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 122 | 123 | 124 | 125 | 126 |

US County Health Rankings

127 | 128 |
129 | 130 | 131 | 132 | 133 | 134 | 379 | 380 | 381 | 382 | 383 | -------------------------------------------------------------------------------- /module-03/lesson-03.md: -------------------------------------------------------------------------------- 1 | # Lesson 02. Thematic Web Mapping: Choropleth Mapping 2 | 3 | This lesson guides you through a process of creating a choropleth map. Choropleth maps are another common type of thematic map that use enumeration units such as states or counties to show how much of a particular phenomenon each contains by proportional shading. These are among the most familiar of thematic map types to the general public, especially for making election maps. 4 | 5 | Choropleth maps are best used to map continuous areal (or area-based) phenomena and represent a statistical surface for enumeration units (i.e., polygons). The map symbology applies a sequence of shaded values (often using color schemes) to symbolize the density or ratio for each enumeration unit on the map. The intensity of the color indicates how much of some phenomenon is within a given area (greater intensity or darker color generally means more of something). 6 | 7 | To make a choropleth map, we need to establish two more pieces of information. First, we need to know the entire range of the particular data value we're encoding. Second, we need to determine the precise values with which we will classify that data range into discrete chunks. We could do some of this analysis prior to creating our GeoJSON file, either through analyzing the data tables within a conventional spreadsheet application such as OpenOffice Calc or Microsoft Excel. We could also run scripts written in JavaScript, Ruby, Python, or R to process our data and create attribute values that are normalized. However, for this module, we are going to do this analysis and determine the class breaks client-side at runtime using JavaScript. 8 | 9 | ## Step 01: get the data 10 | 11 | Note that within the *module-03/app/data/* directory there is a CSV file named *2016CountyHealthRankings.csv*. This data was downloaded from [County Health Rankings & Roadmaps](http://www.countyhealthrankings.org/rankings/data) and edited to preserve some of the data attribute columns already normalized as a rate or percentage. Within the data we see there are US county FIPS codes, but there is no geometry data included. 12 | 13 | To get good polygons useful for mapping, go to[Cartographic Boundary Shapefiles - Counties] (https://www.census.gov/geo/maps-data/data/cbf/cbf_counties.html) and download the [cb_2015_us_county_20m.zip](cb_2015_us_county_20m.zip) Shapefile. Open these Shapefiles and explore on your machine. 14 | 15 | Next, drag or upload the entire zipped shapefile package into the web interface of [MapShaper](http://mapshaper.org/). You can explore various options for simplifying the data and editing the data tables within this tool. For now, simply download your file as a GeoJSON file and save within your *data/* directory. Save it as *counties.json*. 16 | 17 | ## Step 02: load the data 18 | 19 | We can again use JQuery's getJSON method to load the data asynchronously. Load the data and verify that is correct. 20 | 21 | ```JavaScript 22 | $.getJSON('data/counties.json', function(counties) { 23 | console.log(counties) 24 | } 25 | ``` 26 | 27 | To be finished .... 28 | -------------------------------------------------------------------------------- /module-04/README.md: -------------------------------------------------------------------------------- 1 | # Extending Web Maps with Plugins and Web Hosting 2 | 3 | This module introduces you to some additional plugins for extending your thematic web maps. In particular, we'll investigate a couple techniques for dealing with too many point features on a web map. 4 | 5 | Plugins extend an existing library, adding additional functionality to it. There are many designed for use with the [Leaflet library](http://leafletjs.com/plugins.html) and [Mapbox.js](https://www.mapbox.com/blog/extend-your-maps-mapboxjs-plugins/) (which wraps Leaflet.js itself and extends it) 6 | 7 | Plugins explored in this module: 8 | 9 | * [Leafet.markercluster GitHub repo](https://github.com/Leaflet/Leaflet.markercluster) 10 | * [Turf.js](http://turfjs.org/) 11 | * [Leaflet Omnivore](https://github.com/mapbox/leaflet-omnivore) 12 | * [Chroma.js](https://github.com/gka/chroma.js/) 13 | 14 | 15 | ## Leaflet.markercluster.js 16 | 17 | Begin by editing the *index.html* file in the *marker-cluster/* directory. Take a moment to examine the *stations.geojson* data file within the *marker-cluster/data* directory. The data are locations of 13,000 electric vehicle refueling stations, drawn from [NREL's Developer Network API](https://developer.nrel.gov/). 18 | 19 | First, let's begin by loading our data into our script using the now familiar technique of JQuery's *getJSON()* method: 20 | 21 | ```javascript 22 | // request geojson data 23 | $.getJSON('data/stations.geojson', function(data) { 24 | 25 | console.log(data) 26 | 27 | }); 28 | ``` 29 | 30 | We note that the data are loaded as 13,000 Point Features. 31 | 32 | We could try drawing these as markers to the map (go ahead!), but we'll likely run into some rendering issues. There are too many points! 33 | 34 | ```javascript 35 | // request geojson data 36 | $.getJSON('data/stations.geojson', function(data) { 37 | 38 | makeMap(data); 39 | 40 | }); 41 | 42 | function makeMap(data) { 43 | 44 | // too many markers! 45 | L.geoJson(data).addTo(map); 46 | 47 | } 48 | ``` 49 | 50 | To solve this problem we'll use a technique known as "marker clustering." Go to the [Leafet.markercluster GitHub repo](https://github.com/Leaflet/Leaflet.markercluster) and examine the documentation. 51 | 52 | We'll want to grab two CSS libraries and one JS lib. Copy the URLs to the CDNs for the CSS and place in the head of your document (along with Leaflet's CSS): 53 | 54 | 55 | ```javascript 56 | 57 | 58 | 59 | ``` 60 | 61 | Then do the same for the JavaScript (adding this to the existing JQuery and Leaflet JS) toward the bottom of our script. 62 | 63 | ```javascript 64 | 65 | 66 | 67 | ``` 68 | 69 | Now that we have the markercluster CSS and JS available to us, we'll first create an empty *markerClusterGroup*. If we dig into the API docs we'll discover that this object extends Leaflet's L.geoJson object. 70 | 71 | ```javascript 72 | // create new empty markers cluster group 73 | var markers = L.markerClusterGroup(); 74 | ``` 75 | 76 | The markercluster requires Leaflet L.marker's added to it. So, we need to iterate through our data, create a new L.marker object for each Point feature, and add it to this new L.markerClusterGroup we've created. 77 | 78 | To do so we can use the native JavaScript method *forEach()*: 79 | 80 | ```javascript 81 | // for all our data features 82 | data.features.forEach(function(d, i) { 83 | 84 | // create a new Leaflet marker 85 | var marker = L.marker(L.latLng(d.geometry.coordinates[1], d.geometry.coordinates[0])); 86 | 87 | // add the marker to the markerClusterGroup 88 | markers.addLayer(marker); 89 | }); 90 | 91 | // add the makerclusters to the map 92 | markers.addTo(map); 93 | }); 94 | ``` 95 | 96 | Once this is complete, we simply add our L.markerClusterGroup to the map and wa-lah! We have a functioning markercluser map! 97 | 98 | ```javascript 99 | // add the makerclusters to the map 100 | markers.addTo(map); 101 | ``` 102 | 103 | We can include some additional information about each feature if we'd like, displayed in Leaflet's tooltip. Before we add each marker to the *markers* object, we can include code accessing each feature's properties: 104 | 105 | ```javascript 106 | // create a new Leaflet marker 107 | var marker = L.marker(L.latLng(d.geometry.coordinates[1], d.geometry.coordinates[0])); 108 | 109 | // create info for tooltip 110 | var props = d.properties; 111 | var popupTemplate = 112 | 'station name: ' + props.station_name + '
' + 113 | 'city: ' + props.city + '
' + 114 | 'hours: ' + props.access_days_time + '
' + 115 | 'phone: ' + props.station_phone + '
' + 116 | 'address: ' + props.street_address 117 | 118 | // bind the tooltip to the marker 119 | marker.bindTooltip(popupTemplate); 120 | 121 | // add the marker to the markerClusterGroup 122 | markers.addLayer(marker); 123 | ``` 124 | 125 | Throw a title on the map and ship it! Nice work. 126 | 127 | ## Turf.js 128 | 129 | Next we're going to explore a really fun library (not necessarily a plugin extention particular to Leaflet, but integrates nicely): [Turf.js](http://turfjs.org/). Think of Turf as GIS operations and geoprocessing in JavaScript. 130 | 131 | Begin with the index in the *turf-hex/* directory. 132 | 133 | Once again, we have some point data we'd like to map. This time, it's some traffic accident data in Denver from 2016, drawn from the [Denver Open Data Catalog](https://www.denvergov.org/opendata). The data are stored as CSV and contain 21,620 lat/lon values for accident locations. Obviously once again, these are too many to draw to the Leaflet map as markers. 134 | 135 | As usual, our first step is to get the data into the application. For this, we'll make use of a plugin called [Leaflet Omnivore](https://github.com/mapbox/leaflet-omnivore). Take some time to explore the documentation and examples. 136 | 137 | We'll need to load it into our script (there is no CSS for this plugin). 138 | 139 | ```javascript 140 | 141 | ``` 142 | 143 | Onmivore will load our CSV data asynchronously and convert it directly into a Leaflet GeoJson object. 144 | 145 | ```javascript 146 | // async request to load point data and convert to Leaflet GeoJSON layer 147 | var accidents = omnivore.csv('data/accidents.csv') 148 | .on('ready', function(d) { 149 | 150 | // when data is loaded 151 | makeMap(data) 152 | }) 153 | .on('error', function(e) { 154 | console.log('oh no something terrible has happened!') 155 | }) 156 | 157 | function makeMap(accidents) { 158 | // we have access to the Leaflet GeoJson here 159 | console.log(accidents); 160 | } 161 | ``` 162 | 163 | Instead of using a marker cluster here, let's consider how we'd make a hexbin map. To do so, we'll use Turf.js. Load Turf.js into our script along with the other JS libs. 164 | 165 | 166 | ```javascript 167 | 168 | 169 | 170 | 171 | ``` 172 | 173 | Looking at [Turf's API docs for hexGrid](http://turfjs.org/docs/#hexgrid) a bounding box to surround our point features. 174 | 175 | ```javascript 176 | // get the sw/ne bounds of the accidents data layer 177 | var bounds = accidents.getBounds(); 178 | 179 | // convert bounds to string rep of bounding coords 180 | var bbox = bounds.toBBoxString(); 181 | 182 | // convert string to array of numbers for bounding coords 183 | var bbox = bbox.split(',').map(function(coord) { 184 | return Number(coord) 185 | }); 186 | 187 | // designate dimension of cell width 188 | var cellWidth = 1; 189 | 190 | // designate units for cell width calculation 191 | var units = 'miles'; 192 | 193 | // create a featurecollection of hex polygons 194 | var hexgrid = turf.hexGrid(bbox, cellWidth, units); 195 | ``` 196 | 197 | Next, we want to use [Turf's inside method](http://turfjs.org/docs/#inside) (which is essentially a point in polygon process). We need our point features converted back into GeoJSON for this (not Leaflet's GeoJson object). 198 | 199 | ```javascript 200 | // convert the accidents Leaflet layer to GeoJSON 201 | var points = accidents.toGeoJSON(); 202 | ``` 203 | 204 | Next we'll use regular JavaScript to loop through all of our polygon features and then all the point features to perform the "inside" test. If a point falls within a polygon, we'll add a count of 1 to that polygon's properties. When we're done, we should have a total count of all points within each polygon stored within its properties. 205 | 206 | ```javascript 207 | // loop through all the hex polygons 208 | hexgrid.features.map(function(hex) { 209 | 210 | // create a property name for each of 'count' 211 | if(!hex.properties.count) hex.properties.count = 0; 212 | 213 | // loop through all the points 214 | points.features.map(function(point) { 215 | 216 | // if a point is inside a hex polygon 217 | if(turf.inside(point,hex)) { 218 | 219 | // add one to the prop count 220 | hex.properties.count++ 221 | } 222 | 223 | }); 224 | }); 225 | ``` 226 | 227 | Test the result with a `console.log()` statement. 228 | 229 | Now we want to actually draw our Leaflet GeoJson object to the map using these data. While we're add it, we can style the hexbins minimally, filter out the ones that have no accident counts within them, and keep add the value of each to an array for later thematic drawing purposes: 230 | 231 | ```javascript 232 | // empty array to store all our counts 233 | var counts = []; 234 | 235 | // create a Leaflet GeoJson layer with newly 236 | // processed polygon data 237 | var hexLayer = L.geoJson(hexgrid, { 238 | style: function(feature) { 239 | return { 240 | color: "white", 241 | weight: .2, 242 | fillOpacity: 1 243 | } 244 | }, 245 | filter: function(feature) { 246 | // if the poly has data 247 | if(feature.properties.count > 0) { 248 | // push that count to the array 249 | counts.push(feature.properties.count); 250 | 251 | // retain that feature for representation 252 | return feature; 253 | } 254 | } 255 | 256 | }).addTo(map); 257 | ``` 258 | 259 | Super! Now we want to make this a thematic map using a similar technique as Module 03's choropleth map. Rather than using Simple Statistics this time to calculate breaks, we'll use another awesome JS library to determine our breaks and create a color generator based on our data. First, load the [Chroma.js](https://github.com/gka/chroma.js/) into the browser. 260 | 261 | ```javascript 262 | 263 | 264 | 265 | ``` 266 | 267 | We can use the `counts` Array we've built of all the counts totals to determine the breaks, using the fancy ckmeans method (5 classes here for now). 268 | 269 | Then we'll build a color generator function we can then use to color our hexbins. 270 | 271 | ```javascript 272 | // use chroma.js to determine class breaks 273 | // using ckmeans method 274 | var breaks = chroma.limits(counts, 'k', 5); 275 | 276 | // create color generator 277 | var colorize = chroma.scale(chroma.brewer.OrRd).domain(breaks).mode('lab'); 278 | ``` 279 | 280 | Next, let's iterate through our existing `hexLayer` (i.e., the Leaflet GeoJson we've drawn to the map as SVG) and update the fill color of each using our color generator. 281 | 282 | ```javascript 283 | // iterate through the layers 284 | hexLayer.eachLayer(function(layer) { 285 | 286 | // color each one according to count # 287 | layer.setStyle({ 288 | fillColor : colorize(layer.feature.properties.count) 289 | }); 290 | 291 | // bind tooltip to retrieve specific info 292 | layer.bindTooltip(String(layer.feature.properties.count)) 293 | 294 | }); 295 | ``` 296 | 297 | Awesome! We can borrow some code from Module 03 as well to toss a legend on the map. We'll call the function to draw a legend at the bottom of our `drawMap()` function body and pass both the `breaks` and the `colorize` function as arguments. 298 | 299 | Then, we can write a drawLegend function: 300 | 301 | ```javascript 302 | function drawLegend(breaks, colorize) { 303 | // create Leaflet control for legend 304 | var legend = L.control({ 305 | position: 'bottomleft' 306 | }); 307 | 308 | // create a div for legend and add it to the map 309 | legend.onAdd = function(map) { 310 | 311 | return L.DomUtil.create('div', 'legend'); 312 | 313 | }; 314 | 315 | legend.addTo(map); 316 | 317 | // select the legend, add a title 318 | var legend = $('.legend').html("

Number of Traffic Accidents in 2016

"); 319 | 320 | // loop through the Array of classification break values 321 | for (var i = 1; i < breaks.length; i++) { 322 | 323 | // get a color for the current class break 324 | var color = colorize(breaks[i]); 325 | 326 | // build legend using breaks and color values 327 | $('.legend').append( 328 | ' ' + 329 | ''); 331 | } 332 | 333 | } 334 | ``` 335 | 336 | Whoomp there it is. Oh wait, probably need to add some CSS rules to the head of the document: 337 | 338 | ```css 339 | .legend { 340 | padding: 6px 8px; 341 | font-size: 1em; 342 | padding: 10px 15px; 343 | background: rgba(256, 256, 256, .8); 344 | border: 1px solid grey; 345 | border-radius: 3px; 346 | max-width: 220px; 347 | } 348 | .legend h3 { 349 | font-size: 1.4em; 350 | font-weight: bold; 351 | line-height: 1em; 352 | color: #3d3d3d; 353 | margin: 10px 10px 10px auto; 354 | text-align: left; 355 | } 356 | 357 | .legend span { 358 | width: 30px; 359 | height: 20px; 360 | float: left; 361 | margin: 0 10px 4px 0; 362 | } 363 | 364 | .legend label { 365 | font-size: 1.1em; 366 | } 367 | 368 | .legend label:after { 369 | content: ''; 370 | display: block; 371 | clear: both; 372 | } 373 | ``` 374 | 375 | 376 | ## Serving your web map on GitHub 377 | 378 | Web hosting is easy on GitHub. Follow these instructions: [GitHub Pages](https://pages.github.com/) 379 | -------------------------------------------------------------------------------- /module-04/marker-cluster/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /module-04/turf-hex/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 74 | 75 | 76 | 77 | 78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 249 | 250 | 251 | 252 | 253 | --------------------------------------------------------------------------------