Skip to content

Commit

Permalink
Update README, rename app
Browse files Browse the repository at this point in the history
  • Loading branch information
v-dvorak committed Apr 22, 2024
1 parent 5639f6f commit 4bdc7ce
Show file tree
Hide file tree
Showing 16 changed files with 590 additions and 506 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# api key
api_key.txt

# pycharm
.idea

# style
style.json

Expand Down
83 changes: 82 additions & 1 deletion README.md
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.
Binary file added docs/cover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/snazzy_showcase.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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.
18 changes: 9 additions & 9 deletions app/Towns/Prague.py → himap/Towns/Prague.py
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
18 changes: 9 additions & 9 deletions app/Towns/TownBase.py → himap/Towns/TownBase.py
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.
74 changes: 37 additions & 37 deletions app/Utils/ApiKeyUtil.py → himap/Utils/ApiKeyUtil.py
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)
Loading

0 comments on commit 4bdc7ce

Please sign in to comment.