mirror of
https://github.com/marceloprates/prettymaps.git
synced 2026-09-26 09:11:55 +02:00
* Replace tutorial notebook with marimo + static docs Jupyter notebooks (.ipynb) are hostile to code review: every cell output is a base64-embedded blob that churns on every run, and a 27 MB examples.ipynb is essentially unreviewable. Moving the canonical tutorial to a git-friendly marimo notebook and a static Markdown walkthrough keeps code review diffs small, makes the source diffable in pull requests, and lets the rendered tutorial live alongside the library on the GitHub Pages site. - Replace notebooks/examples.ipynb with notebooks/tutorial.py (marimo notebook, 35 cells, 15.1 KB). - Add docs/tutorial.md with the same walkthrough rendered as Markdown plus 10 inline PNGs at docs/img/tour-01..tour-10-*.png extracted from the original notebook's cell outputs. - Add Tutorial entry to mkdocs.yml nav; link from docs/index.md and README.md. README's inline tutorial block is replaced with a pointer table to docs/tutorial.md (README: 1062 -> 112 lines). - Add marimo>=0.23.0 to requirements.txt; update .gitignore to keep the new tutorial.py whitelisted and to ignore marimo caches (__marimo__/, *.marimo). - Fix mkdocs.yml site_url placeholder (yourusername.github.io -> marceloprates.github.io) so the GitHub Pages site's canonical links and sitemap point at the real domain. - The 23 unrelated demo notebooks that lived alongside the tutorial are kept untracked in notebooks/untracked/ (gitignored as before). * Restore README tutorial pointer and update Colab link Follow-up to the tutorial-marimo conversion. The previous rebase onto the upstream CI commit (ba59f1f) accidentally dropped the new README.md rewrite because 'git checkout --ours' during a rebase selects the upstream side, not the commit being applied. This restores the 112-line README.md with the tutorial pointer and updates the Colab link from notebooks/examples.ipynb to notebooks/tutorial.py. * Remove obsolete generate-readme CI workflow The generate-readme.yml workflow and the notebooks_to_readme.sh script it depended on were designed to run notebooks/examples.ipynb and regenerate README.md from its cell outputs. Since examples.ipynb is gone (replaced by the marimo notebook and static docs/tutorial.md), this pipeline has no purpose and was failing the PR check. Also removes the 13 orphan temp_readme_*.png files that the CI had committed to pictures/README/ inba59f1f— their source notebook no longer exists.
556 lines
15 KiB
Python
556 lines
15 KiB
Python
import marimo
|
|
|
|
__generated_with = "0.23.15"
|
|
app = marimo.App()
|
|
|
|
|
|
@app.cell
|
|
def _():
|
|
import marimo as mo
|
|
|
|
return (mo,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
# prettymaps
|
|
|
|
A minimal Python library to draw customized maps from [OpenStreetMap](https://www.openstreetmap.org/#map=12/11.0733/106.3078) created using the [osmnx](https://github.com/gboeing/osmnx), [matplotlib](https://matplotlib.org/), [shapely](https://shapely.readthedocs.io/en/stable/index.html) and [vsketch](https://github.com/abey79/vsketch) packages.
|
|
|
|

|
|
|
|
# [](https://marceloprates.github.io/prettymaps/) [](https://pypi.org/project/prettymaps) [](https://www.python.org/) [](LICENSE)
|
|
|
|
This work is [licensed](LICENSE) under a GNU Affero General Public License v3.0 (you can make commercial use, distribute and modify this project, but must **disclose** the source code with the license and copyright notice)
|
|
|
|
## Note about crediting and NFTs:
|
|
- Please keep the printed message on the figures crediting my repository and OpenStreetMap ([mandatory by their license](https://www.openstreetmap.org/copyright)).
|
|
- I am personally **against** NFTs for their [environmental impact](https://earth.org/nfts-environmental-impact/), the fact that they're a [giant money-laundering pyramid scheme](https://twitter.com/smdiehl/status/1445795667826208770) and the structural incentives they create for [theft](https://twitter.com/NFTtheft) in the open source and generative art communities.
|
|
- **I do not authorize in any way this project to be used for selling NFTs**, although I cannot legally enforce it. **Respect the creator**.
|
|
- The [AeternaCivitas](https://magiceden.io/marketplace/aeterna_civitas) and [geoartnft](https://www.geo-nft.com/) projects have used this work to sell NFTs and refused to credit it. See how they reacted after being exposed: [AeternaCivitas](https://github.com/marceloprates/prettymaps/raw/main/pictures/NFT_theft_AeternaCivitas.jpg), [geoartnft](https://github.com/marceloprates/prettymaps/raw/main/pictures/NFT_theft_geoart.jpg).
|
|
- **I have closed my other generative art projects on Github and won't be sharing new ones as open source to protect me from the NFT community**.
|
|
|
|
<a href='https://ko-fi.com/marceloprates_' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://cdn.ko-fi.com/cdn/kofi1.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
|
|
|
|
## As seen on [Hacker News](https://web.archive.org/web/20210825160918/https://news.ycombinator.com/news):
|
|

|
|
## [prettymaps subreddit](https://www.reddit.com/r/prettymaps_/)
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
# Installation
|
|
|
|
## Install locally:
|
|
|
|
Install prettymaps with:
|
|
|
|
```
|
|
pip install prettymaps
|
|
```
|
|
|
|
## Install on Google Colaboratory:
|
|
|
|
Install prettymaps with:
|
|
|
|
```
|
|
!pip install -e "git+https://github.com/marceloprates/prettymaps#egg=prettymaps"
|
|
```
|
|
|
|
Then **restart the runtime** (Runtime -> Restart Runtime) before importing prettymaps.
|
|
|
|
# Run front-end
|
|
|
|
After prettymaps is installed, you can run the front-end (streamlit) application from the prettymaps repository using:
|
|
|
|
```
|
|
streamlit run app.py
|
|
```
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
# Tutorial
|
|
|
|
Plotting with prettymaps is very simple. Run:
|
|
|
|
```python
|
|
prettymaps.plot(your_query)
|
|
```
|
|
|
|
**your_query** can be:
|
|
1. An address (Example: "Porto Alegre"),
|
|
2. Latitude / Longitude coordinates (Example: (-30.0324999, -51.2303767))
|
|
3. A custom boundary in GeoDataFrame format
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _():
|
|
import prettymaps
|
|
|
|
plot_default = prettymaps.plot(
|
|
'Stad van de Zon, Heerhugowaard, Netherlands'
|
|
)
|
|
return (prettymaps,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
You can also choose from different "presets" (parameter combinations saved in JSON files).
|
|
|
|
See below an example using the "minimal" preset:
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
plot_minimal = prettymaps.plot(
|
|
'Stad van de Zon, Heerhugowaard, Netherlands',
|
|
preset='minimal',
|
|
)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
Run
|
|
|
|
```python
|
|
prettymaps.presets()
|
|
```
|
|
|
|
to list all available presets.
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
prettymaps.presets()
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
To examine a specific preset, run:
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
prettymaps.preset('default')
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
Instead of using the default configuration you can customize several parameters. The most important are:
|
|
|
|
- **layers**: A dictionary of OpenStreetMap layers to fetch.
|
|
- Keys: layer names (arbitrary)
|
|
- Values: dicts representing OpenStreetMap queries
|
|
- **style**: Matplotlib style parameters
|
|
- Keys: layer names (the same as before)
|
|
- Values: dicts representing Matplotlib style parameters
|
|
|
|
```python
|
|
plot = prettymaps.plot(
|
|
# Your query. Example: "Porto Alegre" or (-30.0324999, -51.2303767) (GPS coords)
|
|
your_query,
|
|
# Dict of OpenStreetMap Layers to plot. Example:
|
|
# {'building': {'tags': {'building': True}}, 'water': {'tags': {'natural': 'water'}}}
|
|
# Check the /presets folder for more examples
|
|
layers,
|
|
# Dict of style parameters for matplotlib. Example:
|
|
# {'building': {'palette': ['#f00','#0f0','#00f'], 'edge_color': '#333'}}
|
|
style,
|
|
# Preset to load. Options include:
|
|
# ['default', 'minimal', 'macao', 'tijuca']
|
|
preset,
|
|
# Save current parameters to a preset file.
|
|
# Example: "my-preset" will save to "presets/my-preset.json"
|
|
save_preset,
|
|
# Whether to update loaded preset with additional provided parameters. Boolean
|
|
update_preset,
|
|
# Plot with circular boundary. Boolean
|
|
circle,
|
|
# Plot area radius. Float
|
|
radius,
|
|
# Dilate the boundary by this amount. Float
|
|
dilate
|
|
)
|
|
```
|
|
|
|
**plot** is a Python dataclass containing:
|
|
|
|
```python
|
|
@dataclass
|
|
class Plot:
|
|
# A dictionary of GeoDataFrames (one for each plot layer)
|
|
geodataframes: Dict[str, gp.GeoDataFrame]
|
|
# A matplotlib figure
|
|
fig: matplotlib.figure.Figure
|
|
# A matplotlib axis object
|
|
ax: matplotlib.axes.Axes
|
|
```
|
|
|
|
Here's an example of running `prettymaps.plot()` with customized parameters:
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
plot_macau = prettymaps.plot(
|
|
'Praça Ferreira do Amaral, Macau',
|
|
circle=True,
|
|
radius=1100,
|
|
layers={
|
|
"green": {
|
|
"tags": {
|
|
"landuse": "grass",
|
|
"natural": ["island", "wood"],
|
|
"leisure": "park",
|
|
}
|
|
},
|
|
"forest": {"tags": {"landuse": "forest"}},
|
|
"water": {"tags": {"natural": ["water", "bay"]}},
|
|
"parking": {
|
|
"tags": {
|
|
"amenity": "parking",
|
|
"highway": "pedestrian",
|
|
"man_made": "pier",
|
|
}
|
|
},
|
|
"streets": {
|
|
"width": {
|
|
"motorway": 5,
|
|
"trunk": 5,
|
|
"primary": 4.5,
|
|
"secondary": 4,
|
|
"tertiary": 3.5,
|
|
"residential": 3,
|
|
}
|
|
},
|
|
"building": {"tags": {"building": True}},
|
|
},
|
|
style={
|
|
"background": {"fc": "#F2F4CB", "ec": "#dadbc1", "hatch": "ooo..."},
|
|
"perimeter": {"fc": "#F2F4CB", "ec": "#dadbc1", "lw": 0, "hatch": "ooo..."},
|
|
"green": {"fc": "#D0F1BF", "ec": "#2F3737", "lw": 1},
|
|
"forest": {"fc": "#64B96A", "ec": "#2F3737", "lw": 1},
|
|
"water": {
|
|
"fc": "#a1e3ff",
|
|
"ec": "#2F3737",
|
|
"hatch": "ooo...",
|
|
"hatch_c": "#85c9e6",
|
|
"lw": 1,
|
|
},
|
|
"parking": {"fc": "#F2F4CB", "ec": "#2F3737", "lw": 1},
|
|
"streets": {"fc": "#2F3737", "ec": "#475657", "alpha": 1, "lw": 0},
|
|
"building": {
|
|
"palette": ["#FFC857", "#E9724C", "#C5283D"],
|
|
"ec": "#2F3737",
|
|
"lw": 0.5,
|
|
},
|
|
},
|
|
)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
In order to plot an entire region and not just a rectangular or circular area, set
|
|
|
|
```python
|
|
radius = False
|
|
```
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
plot_bomfim = prettymaps.plot(
|
|
'Bom Fim, Porto Alegre, Brasil', radius=False
|
|
)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
You can access layer's GeoDataFrames directly like this:
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
plot_centro = prettymaps.plot(
|
|
'Centro Histórico, Porto Alegre', show=False
|
|
)
|
|
plot_centro.geodataframes['building']
|
|
return (plot_centro,)
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
Search a building by name and display it:
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(plot_centro):
|
|
plot_centro.geodataframes['building'][
|
|
plot_centro.geodataframes['building'].name
|
|
== 'Catedral Metropolitana Nossa Senhora Mãe de Deus'
|
|
].geometry[0]
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
Plot mosaic of building footprints:
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
import numpy as np
|
|
import osmnx as ox
|
|
from matplotlib import pyplot as plt
|
|
|
|
plot_poa = prettymaps.plot('Porto Alegre', show=False)
|
|
buildings = plot_poa.geodataframes['building']
|
|
buildings = ox.projection.project_gdf(buildings)
|
|
buildings = [b for b in buildings.geometry if b.area > 0]
|
|
|
|
n = 6
|
|
fig, axes = plt.subplots(n, n, figsize=(7, 6))
|
|
fig.patch.set_facecolor('#5cc0eb')
|
|
fig.suptitle('Buildings of Porto Alegre', size=25, color='#fff')
|
|
for ax, building in zip(np.concatenate(axes), buildings):
|
|
ax.plot(*building.exterior.xy, c='#ffffff')
|
|
ax.autoscale()
|
|
ax.axis('off')
|
|
ax.axis('equal')
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
Access `plot.ax` or `plot.fig` to add new elements to the matplotlib plot:
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
plot_bcn = prettymaps.plot(
|
|
(41.39491, 2.17557),
|
|
preset='barcelona',
|
|
show=False,
|
|
)
|
|
|
|
plot_bcn.fig.patch.set_facecolor('#F2F4CB')
|
|
_ = plot_bcn.ax.set_title('Barcelona', font='serif', size=50)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
Use **plotter** mode to export a pen plotter-compatible SVG (thanks to abey79's amazing [vsketch](https://github.com/abey79/vsketch) library):
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
plot_bcn_plotter = prettymaps.plot(
|
|
(41.39491, 2.17557),
|
|
mode='plotter',
|
|
layers=dict(perimeter={}),
|
|
preset='barcelona-plotter',
|
|
scale_x=0.6,
|
|
scale_y=-0.6,
|
|
)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
Some other examples:
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
plot_tijuca = prettymaps.plot(
|
|
'Barra da Tijuca',
|
|
dilate=0,
|
|
figsize=(22, 10),
|
|
preset='tijuca',
|
|
adjust_aspect_ratio=False,
|
|
)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
Use `prettymaps.create_preset()` to create a preset:
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
prettymaps.create_preset(
|
|
"my-preset",
|
|
layers={
|
|
"building": {
|
|
"tags": {
|
|
"building": True,
|
|
"leisure": ["track", "pitch"],
|
|
}
|
|
},
|
|
"streets": {
|
|
"width": {
|
|
"trunk": 6,
|
|
"primary": 6,
|
|
"secondary": 5,
|
|
"tertiary": 4,
|
|
"residential": 3.5,
|
|
"pedestrian": 3,
|
|
"footway": 3,
|
|
"path": 3,
|
|
}
|
|
},
|
|
},
|
|
style={
|
|
"perimeter": {"fill": False, "lw": 0, "zorder": 0},
|
|
"streets": {"fc": "#F1E6D0", "ec": "#2F3737", "lw": 1.5, "zorder": 3},
|
|
"building": {
|
|
"palette": ["#fff"],
|
|
"ec": "#2F3737",
|
|
"lw": 1,
|
|
"zorder": 4,
|
|
},
|
|
},
|
|
)
|
|
|
|
prettymaps.preset('my-preset')
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
Use **prettymaps.multiplot** and **prettymaps.Subplot** to draw multiple regions on the same canvas:
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
multiplot_pa = prettymaps.multiplot(
|
|
prettymaps.Subplot(
|
|
'Cidade Baixa, Porto Alegre',
|
|
style={'building': {'palette': ['#49392C', '#E1F2FE', '#98D2EB']}},
|
|
),
|
|
prettymaps.Subplot(
|
|
'Bom Fim, Porto Alegre',
|
|
style={'building': {'palette': ['#BA2D0B', '#D5F2E3', '#73BA9B', '#F79D5C']}},
|
|
),
|
|
prettymaps.Subplot(
|
|
'Farroupilha, Porto Alegre',
|
|
layers={'building': {'tags': {'building': True}}},
|
|
style={'building': {'palette': ['#EEE4E1', '#E7D8C9', '#E6BEAE']}},
|
|
),
|
|
preset='cb-bf-f',
|
|
figsize=(12, 12),
|
|
)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
# Add hillshade
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
plot_honolulu = prettymaps.plot(
|
|
'Honolulu',
|
|
radius=5500,
|
|
figsize='a4',
|
|
layers={
|
|
'hillshade': {
|
|
'azdeg': 315,
|
|
'altdeg': 45,
|
|
'vert_exag': 1,
|
|
'dx': 1,
|
|
'dy': 1,
|
|
'alpha': 0.75,
|
|
},
|
|
},
|
|
)
|
|
return
|
|
|
|
|
|
@app.cell(hide_code=True)
|
|
def _(mo):
|
|
mo.md(r"""
|
|
# Add keypoints
|
|
""")
|
|
return
|
|
|
|
|
|
@app.cell
|
|
def _(prettymaps):
|
|
plot_garopaba = prettymaps.plot(
|
|
'Garopaba',
|
|
radius=5000,
|
|
figsize='a4',
|
|
layers={'building': False},
|
|
keypoints={
|
|
'tags': {'natural': ['beach']},
|
|
'specific': {
|
|
'pedra branca': {'tags': {'natural': ['peak']}},
|
|
},
|
|
},
|
|
)
|
|
return
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run()
|