Skip to content

Commit

Permalink
First version of mapy
Browse files Browse the repository at this point in the history
  • Loading branch information
felix-ht committed Nov 25, 2024
0 parents commit 66fd938
Show file tree
Hide file tree
Showing 75 changed files with 19,706 additions and 0 deletions.
Binary file added .coverage
Binary file not shown.
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 88
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.vscode
*.pyc
.DS_Store
7 changes: 7 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2024 OCELL

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
228 changes: 228 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# Mapy Project

## Overview
Mapy is a Python library designed easily render static maps in python. It is designed to be simple to use and easy to integrate with existing codebases. The library supports rendering background, tiled raster, filled polygon, and other layers on the map. It directly supports geometric primitives, allowing users to directly render shapely geometries.

Input data must be in the `EPSG:4326` - WGS84 projection.

### Supported Layer Types

| type | status | description | data source|
| ---- | ------ | ----------- | -------- |
| `BackgroundLayer` || renders a simple background with a single color | Color |
| `TiledRasterLayer` || renders a tiled raster layer can can load xyz tiles | xyz via http(s) |
| `FillLayer` || renders a fill layer for polygons| Polygon and MultiPolygon |
| `LineLayer` || renders a line layer that draw LineStrings | LineString and MultiLineString|
| `CircleLayer` || renders a circle layer for Points | Point and MultiPoint |
| `SymbolLayer` || renders a symbol and/or text for points| Point and MultiPoint |
| `Attribution` || an attribution | str and list[str] |


## Installation
To install the Mapy library, clone the repository and install the required dependencies:

```bash
git clone <repository-url>
cd mapy
poetry install
# or
pip install .
```

This library uses Cairo. You have to install cairo with your package manger of choice.

on mac

```bash
brew install cairo
```

## Usage

The Mapy library is designed to be simple to use. The following sections provide examples of how to create a map with different layers. Note that in almost all cases you would have to add an attribution layer to the map. For example, if you use OpenStreetMap tiles, you would have to add the OpenStreetMap attribution to the map. This is not done automatically!


### Creating a simple Map

Here is an example of how to create a simple map with a filled polygon:

```python
import mapy
my_map = mapy.Map()
tile_layer = mapy.TiledRasterLayer(
[
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
]
)
my_map.add_layer(tile_layer)
my_map.add_layer(Attribution("© OpenStreetMap contributors"))

surf = my_map.render(
mapy.FixedScreenSize(
Box.from_lng_lat(5.988, 47.302, 15.016, 54.983), mapy.ScreenSize(512, 512)
)
)
surf.write_to_png("my_map.png")

```

If you want to use the map on a public place be sure to include proper attribution. This code will generate a map with a filled polygon and save it as `simple_map.png`.



#### Background Layer
A background layer provides a solid color background for the map.

```python
background_layer = mapy.BackgroundLayer(mapy.Color(1, 1, 1))
my_map.add_layer(background_layer)
```

#### Tiled Raster Layer
A tiled raster layer allows the use of map tiles from sources like OpenStreetMap.

```python
tile_layer = mapy.TiledRasterLayer(
[
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
]
)
my_map.add_layer(tile_layer)
```

#### Fill Layer
A fill layer can be used to add filled polygons with customizable colors and borders.

```python
from shapely.geometry import shape

polygon = shape(json)
fill_layer = mapy.FillLayer(
[
mapy.FillItem(
polygon,
fill_color=mapy.Color(0.5, 0.5, 0.5, 0.3),
line_color=mapy.Color(0, 0, 0),
line_width=2,
)
]
)
my_map.add_layer(fill_layer)
```

#### Line Layer
A line layer can be used to show LineStrings on the map

```python
from shapely.geometry import shape

line = shape(json)
fill_layer = mapy.LineLayer(
[
mapy.LineItem(
line,
join=mapy.LineJoin.round,
cap=mapy.LineCap.round
width=12,
outline_width=3,
outline_color=Colors.BLACK,
)
]
)
my_map.add_layer(fill_layer)
```

The `LineItem` options `cap` and `join` lead to the following results:

![Cap Join Options](images/line_types.png)


#### Circle Layer
A circle layer can be used to show Points on the map

```python
from shapely.geometry import shape

point = shape(json)
circle_layer = mapy.CircleLayer(
[
mapy.CircleItem(
point,
fill_color=mapy.Color(0.5, 0.5, 0.5, 0.3),
line_color=mapy.Color(0, 0, 0),
line_width=2,
radius=10,
)
]
)
my_map.add_layer(circle_layer)
```

#### Symbol Layer
A symbol layer can be used to show Points on the map. You can load custom icons by using the `mapy.Icon.from_path` class method.

##### Limitations
- The text is not automatically placed relative to the symbol. You have to calculate the position yourself.
- No collision detection is implemented. If you place multiple symbols with text on top of each other, the text and symbols will overlap. This might get added in the future, but is somewhat complicated to implement.




```python
from shapely.geometry import shape

point = shape(json)
symbol_layer = mapy.SymbolLayer(
[
mapy.SymbolItem(
point,
icon=mapy.Icons.PIN_24,
text="Hello World",
text_offset=(0, 16)
)
]
)
my_map.add_layer(symbol_layer)
```

You can set the anchor of the text with the `text_anchor` parameter. The default is `mapy.TextAnchor.BOTTOM_LEFT`. The following options are available:

| | | |
| - | - | - |
| ![TOP_LEFT](images/text_anchor_top_left.png) | ![TOP](images/text_anchor_top.png) | ![TOP_RIGHT](images/text_anchor_top_right.png) |
| ![LEFT](images/text_anchor_left.png) | ![CENTER](images/text_anchor_center.png) | ![RIGHT](images/text_anchor_right.png) |
| ![BOTTOM_LEFT](images/text_anchor_bottom_left.png) | ![BOTTOM](images/text_anchor_BOTTOM.png) | ![BOTTOM_RIGHT](images/text_anchor_bottom_right.png) |



#### Attribution
An attribution layer can be used to add attribution to the map. This is important if you use tiles from a public source like OpenStreetMap.

```python
attribution = mapy.Attribution("© OpenStreetMap contributors")
my_map.add_layer(attribution)
```



## Testing

The project includes unit tests to ensure the functionality of various components. To run the tests, use the following command:

```bash
pytest
```

## Output Example

The image below is an example of a map created using the Mapy library:

![Enforced Bounding Box](images/EnforcedBBox.png)

## License
This project is licensed under the MIT License.

## Contributing
Contributions are welcome! Please submit a pull request or open an issue for any changes or suggestions.


81 changes: 81 additions & 0 deletions example/complex_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import json
from typing import Any

import mapy
from shapely.geometry import shape, Polygon

import random
from mapy.geo_util import Box, merge_bounds


def load_geojson(file_path: str) -> tuple[list[Polygon], dict[str, Any]]:
with open(file_path, "r") as f:
data = json.load(f)
features = data["features"]
geoms = []
properties = []
for feature in features:
geom = shape(feature["geometry"])
properties.append(feature["properties"])
geoms.append(geom)
return geoms, properties


def build_fill_items(polygons: list[Polygon]) -> list[mapy.FillItem]:
items = []
for poly in polygons:
fill_color = mapy.Color.from_hsv(random.random(), 0.7, 0.5, 0.2)
line_color = mapy.Color(0, 0, 0, 0.6)
line_width = 1
items.append(mapy.FillItem(poly, fill_color, line_color, line_width))
return items


def build_symbol_items(
polygons: list[Polygon], properties: list[dict[str, Any]]
) -> list[mapy.SymbolItem]:
items = []
for poly, props in zip(polygons, properties):
poly.centroid
text = props["NAME_2"]
symbol_item = mapy.SymbolItem(
poly.centroid,
text=text,
text_weight=mapy.FontWeight.BOLD,
text_size=18,
text_color=mapy.Colors.BLACK,
text_outline_color=mapy.Colors.WHITE,
text_outline_width=2,
text_anchor=mapy.TextAnchor.CENTER,
text_offset=(0, 40) if text == "Brandenburg" else (0, 0),
)
items.append(symbol_item)
return items


def main():
map = mapy.Map()
random.seed(0)
geoms, properties = load_geojson("example/districts_germany.json")
bboxes = [Box(*geom.bounds) for geom in geoms]
bbox = merge_bounds(bboxes).with_relative_padding(0.05)

tile_layer = mapy.TiledRasterLayer(
[
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
]
)

map.add_layer(tile_layer)
map.add_layer(mapy.FillLayer(build_fill_items(geoms)))
map.add_layer(mapy.SymbolLayer(build_symbol_items(geoms, properties)))
map.add_layer(mapy.Attribution("© OpenStreetMap contributors"))

render_mode = mapy.FixedScreenSize(bbox, mapy.ScreenSize(1400, 1175))
map.render(render_mode).write_to_png("images/EnforcedScreenSize.png")
render_mode = mapy.FixedBBox(bbox, 1000**2)
map.render(render_mode).write_to_png("images/EnforcedBBox.png")


if __name__ == "__main__":
main()
Loading

0 comments on commit 66fd938

Please sign in to comment.