-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
590 additions
and
506 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
# api key | ||
api_key.txt | ||
|
||
# pycharm | ||
.idea | ||
|
||
# style | ||
style.json | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,83 @@ | ||
# HiMap | ||
downloads high resolution maps with user defined styling using the Google Maps Static API | ||
|
||
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) | ||
|
||
HiMap is a console application that allows you to download Google Maps with custom styling, with any supported zoom and at any size. The program is written in Python and relies on the Google Maps Static API, through which it sequentially requests parts of the map and then uses the PIL library to assemble them into one large image. | ||
|
||
![](/docs/cover.png) | ||
|
||
## Features | ||
|
||
The user specifies the map coordinates (or one coordinate and the total size), zoom level, map style, and enters their API key. The result is a map matching the requirements stored in PNG. | ||
|
||
You can use [Snazzy Maps](https://snazzymaps.com/) to style the map and then download the style as a `JavaScript Style Array` directly from the SM editor. | ||
|
||
<figure> | ||
<img src="/docs/snazzy_showcase.png" alt="" /> | ||
<figcaption>showcase of popular styles from <a href="https://snazzymaps.com/">Snazzy Maps</a></figcaption> | ||
</figure> | ||
|
||
The resulting map has size of `width*640 x height*614` px. | ||
|
||
## Limitations | ||
|
||
Unfortunately, the Earth is not flat, so we have to accept that linear approximations do not work in extreme cases. This approach breaks around the poles, for example, but works perfectly for countries in Europe. | ||
|
||
## Installation and use | ||
|
||
The script is controlled from the command line. You need to have Python `3.11` or higher installed. After cloning the repository from GitHub, we first create a virtual environment and install the necessary packages using commands: | ||
|
||
```bash | ||
# creating venv: | ||
python -m venv .venv | ||
|
||
# venv activation: | ||
# linux | ||
source .venv/bin/activate | ||
# windows cmd | ||
.venv/Scripts/activate.bat | ||
# windows PS | ||
.venv/Scripts/Activate.ps1 | ||
|
||
# installing requirements: | ||
pip install -r requirements.txt | ||
|
||
# run the script | ||
python3 -m himap output_path optional_params | ||
``` | ||
|
||
- `output_path` | ||
- the result will be stored here | ||
- `--start X Y` | ||
- where `X` and `Y` are the latitude and longitude in degrees of the upper left corner of the map | ||
- `X` and `Y` are `float` | ||
- `--end X Y` | ||
- where X and Y are the latitude and longitude in degrees of the bottom right corner of the map | ||
- `X` and `Y` are `float` | ||
- `--width X` | ||
- map width as the number of squares that make up the final map | ||
- `X` is `uint` | ||
- `--height X` | ||
- map height as the number of squares that make up the final map | ||
- `X` is `uint` | ||
- these parameters can be used for debugging (for example, to check the alignment of map pieces: `--width 2 --height 2`) | ||
- `-z X, --zoom X` | ||
- Google Maps zoom level, determines the zoom of the map | ||
- `X` is `unint`, `0 <= X <= 19` | ||
- `--style path_to_style` | ||
- path to `JSON` with styling to be used on the map | ||
- `--save` | ||
- if this flag is set, all parts of the map that the script pulls from the Google server during runtime are saved in the same directory as the final map | ||
- `--key api_key` | ||
- the API key is needed to communicate with the Google server, each user has their own | ||
- `--store` | ||
- saves the specified API key for further use | ||
- there is no need to enter the key when the script is run again, it reads it itself from the created `TXT` file | ||
|
||
Since it is significantly easier to crop the map than to struggle with shifting coordinates because of a few pixels, a padding is added around the specified coordinates, from 320 to around 640 px. | ||
|
||
## :warning: Disclaimer | ||
|
||
Downloading, caching and similar usage of Google Maps violates the TOS, therefore I disclaim any liability arising from the use of this program. More at [Google Maps TOS](https://cloud.google.com/maps-platform/terms?_gl=1*1x7oou1*_ga*MjgzMTU4Njg3LjE3MTI5MjQ3ODQ.*_ga_NRWSTWS78N*MTcxMzUxMjEyOS40LjEuMTcxMzUxMjEzNS4wLjAuMA..#3.-license.), to be more specific: paragraph `3.2.3 a)`. | ||
|
||
The stored API key is not encrypted in any way, it is saved as plain text in the directory from which the script is run. It is therefore visible to everyone who has access to the folder. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
158 changes: 79 additions & 79 deletions
158
app/ImageGenerator/MapGenerator.py → himap/ImageGenerator/MapGenerator.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,79 +1,79 @@ | ||
from tqdm import tqdm | ||
from pathlib import Path | ||
from PIL import Image | ||
|
||
from ..Utils import Utils | ||
|
||
|
||
def make_map(start: tuple[float, float], width: int, height: int, x_growth: float, y_growth: float, | ||
api_key: str, output_path: Path, zoom: int = 16, size: tuple[int, int] = (640, 640), style: str = "", | ||
verbose: bool = False, save_images: bool = False): | ||
""" | ||
Takes in basic information about the map, downloads it and saves in a PNG format. | ||
:param start: start coordinates | ||
:param width: width of the map | ||
:param height: height of the map | ||
:param zoom: zoom of the map | ||
:param x_growth: growth factor of the map along the X coordinate | ||
:param y_growth: growth factor of the map along the Y coordinate | ||
:param api_key: api key | ||
:param output_path: output path | ||
:param zoom: zoom of the map | ||
:param style: style of the map | ||
:param verbose: verbose mode | ||
:param save_images: save images when generating the final map | ||
""" | ||
|
||
home = output_path / ".." | ||
x, y = start | ||
y_base = y | ||
|
||
if save_images: | ||
for i in tqdm(range(height)): | ||
y = y_base | ||
for j in range(width): | ||
file_path = Path(home / (str(i * width + j) + ".png")) | ||
url = Utils.get_static_request(api_key, (x, y), zoom, size, style) | ||
if verbose: | ||
print(url) | ||
Utils.download_static_map(url, file_path, verbose=verbose) | ||
y += y_growth | ||
x -= x_growth | ||
|
||
Utils.generate_matrix_image([home / (str(i) + ".png") for i in range(height * width)], | ||
(height, width), output_path=output_path, resize=True) | ||
else: | ||
rows, columns = height, width | ||
# dimensions of the final image | ||
image_width, image_height = 640, 614 | ||
final_width = image_width * columns | ||
final_height = image_height * rows | ||
|
||
# new blank image | ||
final_image = Image.new('RGBA', (final_width, final_height), color='white') | ||
|
||
final_image.save(output_path) | ||
im_num = 0 | ||
for i in tqdm(range(height)): | ||
y = y_base | ||
for j in range(width): | ||
url = Utils.get_static_request(api_key, (x, y), zoom, size, style) | ||
image = Utils.load_image_from_url(url).crop((0, 0, 640, 614)) | ||
|
||
row = im_num // columns | ||
col = im_num % columns | ||
x_offset = col * image_width | ||
y_offset = row * image_height | ||
|
||
final_image.paste(image, (x_offset, y_offset)) | ||
|
||
if verbose: | ||
print(url) | ||
|
||
im_num += 1 | ||
y += y_growth | ||
x -= x_growth | ||
|
||
final_image.save(output_path) | ||
from tqdm import tqdm | ||
from pathlib import Path | ||
from PIL import Image | ||
|
||
from ..Utils import Utils | ||
|
||
|
||
def make_map(start: tuple[float, float], width: int, height: int, x_growth: float, y_growth: float, | ||
api_key: str, output_path: Path, zoom: int = 16, size: tuple[int, int] = (640, 640), style: str = "", | ||
verbose: bool = False, save_images: bool = False): | ||
""" | ||
Takes in basic information about the map, downloads it and saves in a PNG format. | ||
:param start: start coordinates | ||
:param width: width of the map | ||
:param height: height of the map | ||
:param zoom: zoom of the map | ||
:param x_growth: growth factor of the map along the X coordinate | ||
:param y_growth: growth factor of the map along the Y coordinate | ||
:param api_key: api key | ||
:param output_path: output path | ||
:param zoom: zoom of the map | ||
:param style: style of the map | ||
:param verbose: verbose mode | ||
:param save_images: save images when generating the final map | ||
""" | ||
|
||
home = output_path / ".." | ||
x, y = start | ||
y_base = y | ||
|
||
if save_images: | ||
for i in tqdm(range(height)): | ||
y = y_base | ||
for j in range(width): | ||
file_path = Path(home / (str(i * width + j) + ".png")) | ||
url = Utils.get_static_request(api_key, (x, y), zoom, size, style) | ||
if verbose: | ||
print(url) | ||
Utils.download_static_map(url, file_path, verbose=verbose) | ||
y += y_growth | ||
x -= x_growth | ||
|
||
Utils.generate_matrix_image([home / (str(i) + ".png") for i in range(height * width)], | ||
(height, width), output_path=output_path, resize=True) | ||
else: | ||
rows, columns = height, width | ||
# dimensions of the final image | ||
image_width, image_height = 640, 614 | ||
final_width = image_width * columns | ||
final_height = image_height * rows | ||
|
||
# new blank image | ||
final_image = Image.new('RGBA', (final_width, final_height), color='white') | ||
|
||
final_image.save(output_path) | ||
im_num = 0 | ||
for i in tqdm(range(height)): | ||
y = y_base | ||
for j in range(width): | ||
url = Utils.get_static_request(api_key, (x, y), zoom, size, style) | ||
image = Utils.load_image_from_url(url).crop((0, 0, 640, 614)) | ||
|
||
row = im_num // columns | ||
col = im_num % columns | ||
x_offset = col * image_width | ||
y_offset = row * image_height | ||
|
||
final_image.paste(image, (x_offset, y_offset)) | ||
|
||
if verbose: | ||
print(url) | ||
|
||
im_num += 1 | ||
y += y_growth | ||
x -= x_growth | ||
|
||
final_image.save(output_path) | ||
|
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
from .TownBase import TownBase | ||
|
||
|
||
class Prague(TownBase): | ||
def __init__(self): | ||
self.name = 'Prague' | ||
self.x_growth = 0.008435 | ||
self.y_growth = 0.013725 | ||
self.x = 50.18 | ||
from .TownBase import TownBase | ||
|
||
|
||
class Prague(TownBase): | ||
def __init__(self): | ||
self.name = 'Prague' | ||
self.x_growth = 0.008435 | ||
self.y_growth = 0.013725 | ||
self.x = 50.18 | ||
self.y = 14.22 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
class TownBase: | ||
""" | ||
Stores information about a town | ||
""" | ||
def __init__(self): | ||
self.name: str = 'TownBase' | ||
self.x_growth: float = None | ||
self.y_growth: float = None | ||
self.x: float = None | ||
class TownBase: | ||
""" | ||
Stores information about a town | ||
""" | ||
def __init__(self): | ||
self.name: str = 'TownBase' | ||
self.x_growth: float = None | ||
self.y_growth: float = None | ||
self.x: float = None | ||
self.y: float = None |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,37 @@ | ||
from pathlib import Path | ||
|
||
API_KEY_FILE = "api_key.txt" | ||
|
||
|
||
def load_api_key() -> str: | ||
""" | ||
:return: saved API key | ||
""" | ||
api_key_file = Path(API_KEY_FILE) | ||
if api_key_file.is_file(): | ||
with open(api_key_file, "r") as f: | ||
api_key = f.read().strip() | ||
if is_valid_api_key(api_key): | ||
return api_key | ||
raise FileNotFoundError("API key file not found or invalid") | ||
|
||
|
||
def is_valid_api_key(api_key: str) -> bool: | ||
""" | ||
Checks if API key is valid | ||
:param api_key: API key | ||
:return: True if the API key is valid, False otherwise | ||
""" | ||
return len(api_key) > 0 | ||
|
||
|
||
def save_api_key(api_key: str) -> None: | ||
""" | ||
Saves API key | ||
:param api_key: API key | ||
""" | ||
api_key_file = Path(API_KEY_FILE) | ||
with open(api_key_file, "w") as f: | ||
f.write(api_key) | ||
from pathlib import Path | ||
|
||
API_KEY_FILE = "api_key.txt" | ||
|
||
|
||
def load_api_key() -> str: | ||
""" | ||
:return: saved API key | ||
""" | ||
api_key_file = Path(API_KEY_FILE) | ||
if api_key_file.is_file(): | ||
with open(api_key_file, "r") as f: | ||
api_key = f.read().strip() | ||
if is_valid_api_key(api_key): | ||
return api_key | ||
raise FileNotFoundError("API key file not found or invalid") | ||
|
||
|
||
def is_valid_api_key(api_key: str) -> bool: | ||
""" | ||
Checks if API key is valid | ||
:param api_key: API key | ||
:return: True if the API key is valid, False otherwise | ||
""" | ||
return len(api_key) > 0 | ||
|
||
|
||
def save_api_key(api_key: str) -> None: | ||
""" | ||
Saves API key | ||
:param api_key: API key | ||
""" | ||
api_key_file = Path(API_KEY_FILE) | ||
with open(api_key_file, "w") as f: | ||
f.write(api_key) |
Oops, something went wrong.