Introduction: How to Create Custom Stylized Maps Using OpenStreetMap
In this instructable, I will describe a process by which you can generate your own custom-made stylized maps. A stylized map is a map where the user can specify which data layers are visualized, as well as define the style with which each layer is visualized. I will first describe the process through which you can write software to stylize maps, followed by an example of the Python software I wrote to perform this task.
The following video highlights how I personally generate stylized maps, but keep reading for the intimate details. I'm very excited to see what the community creates!
What is my motivation behind this project?
Quite frankly, I started this project because I thought it would be fun to do. This idea has been rattling around in my mind for the past year, and I finally took the time I needed to bring it to fruition. After a day of prototyping with some basic scripting, I was able to generate extremely promising results - so promising that I knew I needed to formalize my scripts such that others could easily make creations of their own.
My motivation in writing this instructable is due to the fact that I found very minimal information on how to create your own stylized maps from scratch. I hope to share what I've learned with the community.
Resources/Links:
Supplies
- A Python distribution (I used Anaconda & Python 3.6)
- PyQt5 (for the GUI dependencies)
Step 1: Defining the Process I: Downloading the OSM File
When I first began this project, the most glaring question was, "where can I get map data." Naturally, as you'd expect, I immediately thought of Google Maps. After significant research, I discovered that Google really doesn't want people playing with their data, in a creative sense or otherwise. In fact, they explicitly disallow web-scraping from Google Maps.
Fortunately, my despair was short-lived upon my discovery of OpenStreetMap (OSM). OSM is a collaborative project involving people all across the globe contributing data. OSM explicitly permits open-ended usage of their data in the name of Open Source software. As such, visiting the OSM webpage is where the map stylizing journey begins.
After arriving at the OSM website, click on the "Export" tab to show the map export tools. Now, zoom in to view the region with which you're interested in collecting map data. Select the "Manually select a different area" link, which will bring up a box on your screen. Shape and place this box over the region of interest. Once satisfied, click the "Export" button to download your OSM data file.
Note #1: If your selected region contains too much data, you will get an error that you've selected too many nodes. If this happens to you, click the "Overpass API" button to download your larger file.
Note #2: If your downloaded OSM file is larger than 30MB, the Python program I wrote will noticeably slow. If you're determined to use a large region, consider writing a script to throw away superfluous data you're not planning to draw.
Step 2: Defining the Process II: Understanding the Data
"I have the data...now what?"
Begin by opening your downloaded OSM file into your favorite text editing software. You will first notice this is an XML file, which is great! XML is easy enough to parse. The beginning of your file should look nearly-identical to the first picture of this step - some basic metadata and geographic boundaries will be listed.
As you scroll the file, you will notice three data elements used throughout:
- Nodes
- Ways
- Relations
The most basic data element, a node simply has a unique identifier, latitude, and longitude associated with it. Of course, there is additional metadata, but we can safely discard it.
Ways are collections of nodes. A way can be rendered as an enclosed shape or as an open-ended line. Ways consist of a collection of nodes identified by their unique identifier. They are tagged with keys that define the data group they belong to. For example, the way pictured in the third image above belongs to data group "place," and its subgroup, "island." In other words, this particular way belongs to the "island" layer under the "place" group. Ways also have unique identifiers.
Lastly, relations are collections of ways. A relation can represent a complex shape with holes or with multiple regions. Relations will also have a unique identifier and will be tagged similarly to ways.
You can read more about these data elements from the OSM wiki:
Step 3: Defining the Process III: Digesting the Data
Now you should have at least a superficial understanding of the data elements that make up an OSM file. At this point, we're interested in reading the OSM data using your language of choice. While this step is Python-centric, if you don't want to use Python, you should still read this part as it contains a few tips and tricks.
The xml package is included by default with most standard Python distributions. We will use this package to very easily parse our OSM file as shown in the first image. In a single for loop, you can process the handling of OSM data for each particular data element.
On the final line of the image, you'll notice I check for the 'bounds' tag. This step is vitally important in translating the latitude and longitude values into pixels on the screen. I highly recommend running this conversion at the time you load the OSM file, as the mass conversion of data is process intensive.
Speaking of converting latitudes and longitudes to screen coordinates, here is a link to the computation function I wrote. You will likely notice something a little strange in converting latitude to screen coordinates. There is an extra step involved when compared to longitude! As it turns out, OSM data is modeled using the Pseudo-Mercator projection method. Fortunately, OSM has fantastic documentation about this topic here, and they provide the latitude conversion functions for a significant number of languages. Awesome!
Note: In my code, screen coordinate (0, 0) is the upper left-hand corner of the screen.
Step 4: Python Map Stylizer Implementation
Up until this point, I have discussed the OSM data file - what it is, how to read it, and what to do with it. Now I will discuss the software I wrote to tackle stylistic map visualization (GitHub repo provided in the introduction).
My specific implementation focuses on user control of the rendering pipeline. Specifically, I allow the user to select the layers they want visible and how they want that layer to be visualized. As I briefly mentioned earlier, there are two classes of elements rendered: fill items and line items. Fills are defined only by a color, while lines are defined by color, line width, line style, line cap style, and line join style.
As the user makes modifications to layer styles and visibility, the changes are reflected in the map widget to the right. Once a user has modified the map's appearance to their satisfaction, he can adjust the maximum map dimension and save the map as an image on his computer. In saving an image, a user configuration file will also be saved. This ensures a user can recall and reuse the configuration he used to generate an particular image at any time.
Step 5: Implementation Drawback + Solution
When I first began stylizing a map manually, I learned this was a rather tedious process. Offering the user maximum control can be simply overwhelming due to the large number of available "knobs." However, there is a simple solution, which involves a little extra scripting.
I started off by identifying which layers I am particularly interested in. For the purpose of this instructable, let's say I'm most interested in buildings (all of them), rivers, main highways, and surface streets. I would write a script where I create an instance of Configuration, toggle layer states appropriately using the setItemState() function and defined constants, and set colors based on how I'd like my layers to appear using the setValue(). The resulting configuration file that gets saved can be copied into the configs folder and loaded by the user.
An example script is in the image above. The second image is a sample of what the helper functions would look like, and since they're basically all identical, just with varying constants, I only included a picture of one example.
Step 6: Areas for Improvement
After reflecting on my software implementation, I've identified several areas that would be helpful improvements for power users.
- Dynamic layer rendering. Currently, I have a predefined list of layers that will be rendered, that's it. Part of the justification was the difficulty in determining whether a layer should be a line or a fill. As a result, with nearly every OSM file you open, you will be greeted with a slew of warnings about layers that will not be rendered. Often these are so minimal it's not an issue, but there are bound to be critical layers missing. Dynamic layer rendering would eliminate these concerns.
- Dynamic layer assignment. This goes hand-in-hand with #1; if you want dynamic layer rendering, you need dynamic layer assignment (i.e., identifying a fill layer vs. a line layer). This could reasonably be accomplished, as I've learned, because Ways whose first and last node are the same will be enclosed paths and therefore filled.
- Color Groups. A stylized map often has several layers that have the same style, and enabling the user to modify a group's style all at the same time would greatly decrease the user's time spent editing layers one-by-one.
Step 7: Closing Thoughts
Thank you everyone for taking the time to read through my Instructable. This project represents the culmination of many hours of research, design, programming, and debugging. I hope I've been able to provide a launch pad from which you can build your own project or build on what I've already written. I also hope my shortcomings and tips provide plenty of points to consider in your design. If you're less inclined to program and more inclined to create works of art, I'd love to see what you make in the comments! The possibilities are endless!
Special thanks to the OpenStreetMap contributors! Projects like this would not be possible without their significant efforts.
Please let me know if you have any questions in the comments!