Web Mapping

Are you viewing this on a mobile device? Try it full screen for the best experience here.

Designed for the Marin County Breeding Bird Atlas, the first tab of this app gives all of the reported breeding birds for the county by species. The second tab shows a coverage map of where we need volunteers, as well as a few other data products to help guide efforts such as a 'print map' option for volunteers to download a PNG of their study area. The third tab provides species lists of likely species by their abbreviated code, and a download link for KMZ so that volunteers can load their study area onto their phones. The last tab provides information and useful links. This app was originally prototyped in Rstudio, however it became immediately clear that scaling an R app would be an issue, so I re-wrote the app in JavaScript and PHP. JavaScript has the benefit of being able to easily be hosted in a static web page. It also has the benefit of database integration through PHP, so this map is connected to a database on the backend making updates and organizing data that much easier.

This map uses HTML and CSS to style the page, the Leaflet JavaScript library to display the map, PHP to connect to the Postgres database, and SQL to query the database. The Postgres database also has expanded spatial analysis functionality through the PostGIS extension, allowing for SQL queries to take on some of the analysis. Want to check out a sample the SQL used in this web map? I've included it at the bottom of this page. Want to see this map in action on the project site? Check it out here: https://marinaudubon.org/birds/marin-county-breeding-bird-atlas/

Current data that volunteers are collecting out in the field right now are still being proofed and processed in R. I have made several R tools to help in this process. Here is a data query tool I made to help data proofers filter and download datasets: https://mcbba2.shinyapps.io/datasearch/

Also here are some web maps I made for our 2021 annual report: Volunteer Hours and Percent Complete Map. The 'percent complete' map uses fixed breaks because project leaders wanted the map to lump any blocks over 60% complete together. However, the 'volunteer hours' map uses quantile breaks so that there is always an equal number of blocks in each bin. This keeps the map even looking and allows for the color breaks to automatically adjust as new project data comes in.

Finally, as a fun out-reach map for 2021 I created a story map for volunteers to submit their photos to. The most interesting parts of this map are actually on the back-end. I wanted volunteers to be able to submit their photos and write their own stories for the story map. So I created a google form which allowed them to submit their sightings, then connected this form to my story map using the google sheets API. I then geocoded all the centroids of the blocks to their block name so that as volunteers enter their block name into the form, the map is able to automatically drop a pin at each photo location. What we end up with is a fully automated story map with data coming in from volunteers and being displayed in real time. Check it out here:


Digitizing from historic paper maps to a modern web map

I took on this project for the San Francisco Christmas Bird Count. They had been using the same paper maps cut out of old road atlas's for years. The vast majority of the volunteers (~150 a year) had never seen the complete map - only knowing their area from their paper map. Taking the scanned maps and creating preliminary maps in ArcPro was just the first step. Some of the boundaries weren't as hard and fast as they seemed on paper, and much of the remaining work was collaborating with region leaders and adjusting boundaries as the surveyors knew them.

Although made in ESRI ArcPro, I chose to publish in QGIS to ensure the map would continue to be available to volunteers well into the future (QGIS cloud has no time limits or hosting costs). This platform also supports location monitoring so volunteers can load the map on their phone and explore the boundaries of their survey area. Having a cohesive complete map in one place ensures standardization across surveys, and allows for surveyors to report species outside of their designated survey areas more easily. Although QGIS does support iframes, I chose not to insert the live map in this page to limit the amount of pop-ups you have to see. But you totally should check out the finished map here:

Spatial Data Science

Pictured here is an app I wrote in the R programming language to visualize air quality data. This app reads in air quality data being streamed from Purple Airs API. Then I derive a raster surface interpolation based on the sensor values of each instrument using the inverse distance weighted (IDW) method. This statistical surface interpolation method is based on the theory of "Global Morans" which states that areas near areas of known values will have similar values. IDW fills missing values between sensors and derives a smooth surface from them. This is achieved by giving values nearest missing data a higher weight than those further away. In the first app I centered our area of interest on the Bay Area of California and allow the user to choose many of the IDW settings. In the second app I center the app around the greater Salt Lake area and streamline the app for ease of use.

When creating a surface interpolation resolution is such an important input that here I have decided to put the resolution on a slider to allow the user to decide for themself at what scale they prefer to set the pixel size. In a surface interpolation the resolution sets the focal statistic of what's averaged within each pixel. After all, models simplify data, so too fine of a resolution actually doesn't tell us much about what's going on, and can slow loading times. But too coarse doesn't help much either. For the purposes of the first app here I felt that allowing the user to set the resolution was just more fun. Change the resolution and see how your interpretation of the air quality changes based on how sensors are averaged and interpolated. Keep your eye on the legend when in a continuous color ramp as it changes based on the range of values available. Finer resolution and higher power interpolations will slow loading time. The second app is streamlined for fast loading times and the resolution/power are tailored to the size of the study area. Feel free to try both out.

What's up with the Mercator Projection?

I'm sorry because of how click and drag works this map does not function on mobile devices.

As a geospatial scientist I often run into the situation where I'm explaining to people what map projections are. There is no way to actually make a map without flattening the round features of the globe. No matter what projection you use there will always be distortions, this is especially true the larger scale the map. And most analysts in my field will admit that the absolute worst projection is the Mercator.

The problem with the Mercator projection is that it tends to inflate the size of objects the further away from the equator you go. Since this is the projection that mobile phones use, this is how most people end up viewing and thinking about the world around them.

I like relating this concept to peeling an orange. There's a million ways you could peel an orange, each one of them may have fewer tears in certain areas, but none of them end up with a square orange peel. This is where the Mercator projections (and really all projections) fail. Except instead of tears like on my orange, you have size distortions.

This is a map that shows that. The pull down in the upper right corner allows you to select a country. That country will stay on the map if you want to select more. Move them around and see how your perception changes based on how countries nearest to the poles get smaller and equatorial nations get larger. Once you realize that Greenland is no bigger than India you may start to rethink what roles 1st world nations play in the world.


SQL Code

This SQL query was written to give the number of blocks occupied by each species breeding in Marin County. The {where} variable on the 2nd to last line of code is so that PHP may insert a WHERE statement into the SQL to filter the results to just one species, although this is optional and this code will run just fine without it. You can see on the 4th and 8th lines of code this SQL query does differentiate between projects and returns a different count depending on which project the species was observed in. You can even see a little PostGIS in this code where I state: ST_AsGeoJSON(wkb_geometry, 5). The "ST_AsGeoJSON()" command converts my geometry that is stored in my database into GeoJSON to be displayed on my webmap. Although this particular SQL query doesn't output any JSON I left this code remnant in my nested SQL as it is used elsewhere in my code to do this. To see this code in action simply search for a species in the app at the top of this page. In the attributes pop-up you will see the results of this exact query filtered using a where statement that is dependent on the species you select.

SELECT   spec AS species, 
         Sum( 
         CASE 
                  WHEN projectcode = 'S' THEN 1 
         END) AS shuford, 
         Sum( 
         CASE 
                  WHEN projectcode = 'E' THEN 1 
         END) AS ebird 
FROM     ( 
                   SELECT    * 
                   FROM      ( 
                                       SELECT    blocks.name, 
                                                 commonname AS common_name, 
                                                 projectcode::varchar(1), 
                                                 ST_AsGeoJSON(wkb_geometry, 5) AS geojson 
                                       FROM      historic 
                                       LEFT JOIN list19p 
                                       ON        species = spec 
                                       LEFT JOIN blocks 
                                       ON        block = name 
                                       WHERE     breeding_code::integer > 1 
                                       UNION 
                                       SELECT    blocks.name, 
                                                 common_name, 
                                                 projectcode::varchar(1), 
                                                 ST_AsGeoJSON(wkb_geometry, 5) AS geojson 
                                       FROM      ebirdjuly 
                                       LEFT JOIN blocks 
                                       ON        blocks.name = ebirdjuly.name) AS nest 
                   LEFT JOIN list19p 
                   ON        common_name = commonname) AS mcbba {$where} 
GROUP BY spec

Get in touch

Any questions at all just drop me a line, I usually respond within a couple days.