mirror of
https://github.com/0015/map_tiles.git
synced 2026-01-17 09:06:59 +01:00
feat: Release version 1.2.0 with new features
This commit is contained in:
@@ -31,7 +31,7 @@ You can easily add this component to your project using the idf.py command or by
|
|||||||
From your project's root directory, simply run the following command in your terminal:
|
From your project's root directory, simply run the following command in your terminal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
idf.py add-dependency "0015/map_tiles^1.1.0"
|
idf.py add-dependency "0015/map_tiles^1.2.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will automatically add the component to your idf_component.yml file and download the required files the next time you build your project.
|
This command will automatically add the component to your idf_component.yml file and download the required files the next time you build your project.
|
||||||
@@ -43,7 +43,7 @@ Add to your project's `main/idf_component.yml`:
|
|||||||
dependencies:
|
dependencies:
|
||||||
map_tiles:
|
map_tiles:
|
||||||
git: "https://github.com/0015/map_tiles.git"
|
git: "https://github.com/0015/map_tiles.git"
|
||||||
version: "^1.1.0"
|
version: "^1.2.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace: "0015"
|
namespace: "0015"
|
||||||
name: "map_tiles"
|
name: "map_tiles"
|
||||||
version: "1.1.0"
|
version: "1.2.0"
|
||||||
maintainers:
|
maintainers:
|
||||||
- Eric Nam <thatprojectstudio@gmail.com>
|
- Eric Nam <thatprojectstudio@gmail.com>
|
||||||
description: "Map tiles component for LVGL 9.x - Load and display map tiles with GPS coordinate conversion"
|
description: "Map tiles component for LVGL 9.x - Load and display map tiles with GPS coordinate conversion"
|
||||||
|
|||||||
49
script/README.md
Normal file
49
script/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# **LVGL Map Tile Converter**
|
||||||
|
|
||||||
|
This Python script is designed to convert a directory of PNG map tiles into a format compatible with LVGL (Light and Versatile Graphics Library) version 9\. The output is a series of binary files (.bin) with a header and pixel data in the RGB565 format, optimized for direct use with LVGL's image APIs.
|
||||||
|
|
||||||
|
The script is multithreaded and can significantly speed up the conversion process for large tile sets.
|
||||||
|
|
||||||
|
## **Features**
|
||||||
|
|
||||||
|
* **PNG to RGB565 Conversion**: Converts standard 24-bit PNG images into 16-bit RGB565.
|
||||||
|
* **LVGL v9 Compatibility**: Generates a .bin file with the correct LVGL v9 header format.
|
||||||
|
* **Multithreaded Conversion**: Utilizes a ThreadPoolExecutor to process tiles in parallel, configurable with the \--jobs flag.
|
||||||
|
* **Directory Traversal**: Automatically finds and converts all .png files within a specified input directory structure.
|
||||||
|
* **Skip Existing Files**: Skips conversion for tiles that already exist in the output directory unless the \--force flag is used.
|
||||||
|
|
||||||
|
## **Requirements**
|
||||||
|
|
||||||
|
The script requires the Pillow library to handle image processing. You can install it using pip:
|
||||||
|
```bash
|
||||||
|
pip install Pillow
|
||||||
|
```
|
||||||
|
|
||||||
|
## **Usage**
|
||||||
|
|
||||||
|
The script is a command-line tool. You can run it from your terminal with the following arguments:
|
||||||
|
```bash
|
||||||
|
python lvgl_map_tile_converter.py --input <input_folder> --output <output_folder> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Arguments**
|
||||||
|
|
||||||
|
* \-i, \--input: **Required**. The root folder containing your map tiles. The script expects a tile structure like zoom/x/y.png.
|
||||||
|
* \-o, \--output: **Required**. The root folder where the converted .bin tiles will be saved. The output structure will mirror the input: zoom/x/y.bin.
|
||||||
|
* \-j, \--jobs: **Optional**. The number of worker threads to use for the conversion. Defaults to the number of CPU cores on your system. Using more jobs can speed up the process.
|
||||||
|
* \-f, \--force: **Optional**. If this flag is set, the script will re-convert all tiles, even if the output .bin files already exist.
|
||||||
|
|
||||||
|
### **Examples**
|
||||||
|
|
||||||
|
**1\. Basic conversion with default settings:**
|
||||||
|
```bash
|
||||||
|
python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1
|
||||||
|
```
|
||||||
|
**2\. Conversion using a specific number of threads (e.g., 8):**
|
||||||
|
```bash
|
||||||
|
python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1 --jobs 8
|
||||||
|
```
|
||||||
|
**3\. Forcing a full re-conversion:**
|
||||||
|
```bash
|
||||||
|
python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1 --force
|
||||||
|
```
|
||||||
178
script/lvgl_map_tile_converter.py
Normal file
178
script/lvgl_map_tile_converter.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import argparse
|
||||||
|
import threading
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from PIL import Image # pip install pillow
|
||||||
|
|
||||||
|
# No implicit defaults; these are set from CLI in main()
|
||||||
|
INPUT_ROOT = None
|
||||||
|
OUTPUT_ROOT = None
|
||||||
|
|
||||||
|
|
||||||
|
# Convert RGB to 16-bit RGB565
|
||||||
|
def to_rgb565(r, g, b):
|
||||||
|
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
|
||||||
|
|
||||||
|
|
||||||
|
# Strip .png or .bin extensions
|
||||||
|
def clean_tile_name(filename):
|
||||||
|
name = filename
|
||||||
|
while True:
|
||||||
|
name, ext = os.path.splitext(name)
|
||||||
|
if ext.lower() not in [".png", ".bin", ".jpg", ".jpeg"]:
|
||||||
|
break
|
||||||
|
filename = name
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
# Create LVGL v9-compatible .bin image
|
||||||
|
def make_lvgl_bin(png_path, bin_path):
|
||||||
|
im = Image.open(png_path).convert("RGB")
|
||||||
|
w, h = im.size
|
||||||
|
pixels = im.load()
|
||||||
|
|
||||||
|
stride = (w * 16 + 7) // 8 # bytes per row (RGB565 = 16 bpp)
|
||||||
|
flags = 0x00 # no compression, no premult
|
||||||
|
color_format = 0x12 # RGB565
|
||||||
|
magic = 0x19
|
||||||
|
|
||||||
|
header = bytearray()
|
||||||
|
header += struct.pack("<B", magic)
|
||||||
|
header += struct.pack("<B", color_format)
|
||||||
|
header += struct.pack("<H", flags)
|
||||||
|
header += struct.pack("<H", w)
|
||||||
|
header += struct.pack("<H", h)
|
||||||
|
header += struct.pack("<H", stride)
|
||||||
|
header += struct.pack("<H", 0) # reserved
|
||||||
|
|
||||||
|
body = bytearray()
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(w):
|
||||||
|
r, g, b = pixels[x, y]
|
||||||
|
rgb565 = to_rgb565(r, g, b)
|
||||||
|
body += struct.pack("<H", rgb565)
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(bin_path), exist_ok=True)
|
||||||
|
|
||||||
|
if os.path.isdir(bin_path):
|
||||||
|
# In case a folder mistakenly exists where a file should
|
||||||
|
print(f"[Fix] Removing incorrect folder: {bin_path}")
|
||||||
|
os.rmdir(bin_path)
|
||||||
|
|
||||||
|
with open(bin_path, "wb") as f:
|
||||||
|
f.write(header)
|
||||||
|
f.write(body)
|
||||||
|
|
||||||
|
print(f"[OK] {png_path} → {bin_path}")
|
||||||
|
|
||||||
|
|
||||||
|
# Yield (input_path, output_path) pairs for all PNG tiles under INPUT_ROOT
|
||||||
|
def _iter_tile_paths():
|
||||||
|
for zoom in sorted(os.listdir(INPUT_ROOT)):
|
||||||
|
zoom_path = os.path.join(INPUT_ROOT, zoom)
|
||||||
|
if not os.path.isdir(zoom_path) or not zoom.isdigit():
|
||||||
|
continue
|
||||||
|
|
||||||
|
for x_tile in sorted(os.listdir(zoom_path)):
|
||||||
|
x_path = os.path.join(zoom_path, x_tile)
|
||||||
|
if not os.path.isdir(x_path) or not x_tile.isdigit():
|
||||||
|
continue
|
||||||
|
|
||||||
|
for y_file in sorted(os.listdir(x_path)):
|
||||||
|
if not y_file.lower().endswith(".png"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
tile_base = clean_tile_name(y_file)
|
||||||
|
input_path = os.path.join(INPUT_ROOT, zoom, x_tile, y_file)
|
||||||
|
output_path = os.path.join(OUTPUT_ROOT, zoom, x_tile, f"{tile_base}.bin")
|
||||||
|
yield input_path, output_path
|
||||||
|
|
||||||
|
|
||||||
|
def convert_all_tiles(jobs=1, force=False):
|
||||||
|
"""
|
||||||
|
Convert tiles with optional threading.
|
||||||
|
- jobs: number of worker threads (>=1)
|
||||||
|
- force: if True, re-generate even if output exists
|
||||||
|
"""
|
||||||
|
if not os.path.isdir(INPUT_ROOT):
|
||||||
|
print(f"[ERROR] '{INPUT_ROOT}' not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Build task list (skip existing unless --force)
|
||||||
|
tasks = []
|
||||||
|
for input_path, output_path in _iter_tile_paths():
|
||||||
|
if not force and os.path.isfile(output_path):
|
||||||
|
print(f"[Skip] {output_path}")
|
||||||
|
continue
|
||||||
|
tasks.append((input_path, output_path))
|
||||||
|
|
||||||
|
if not tasks:
|
||||||
|
print("[INFO] Nothing to do.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"[INFO] Converting {len(tasks)} tiles with {jobs} thread(s)...")
|
||||||
|
|
||||||
|
if jobs <= 1:
|
||||||
|
# Serial path
|
||||||
|
for inp, outp in tasks:
|
||||||
|
try:
|
||||||
|
make_lvgl_bin(inp, outp)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Error] Failed to convert {inp} → {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Threaded path
|
||||||
|
print_lock = threading.Lock()
|
||||||
|
with ThreadPoolExecutor(max_workers=jobs) as ex:
|
||||||
|
future_map = {ex.submit(make_lvgl_bin, inp, outp): (inp, outp) for inp, outp in tasks}
|
||||||
|
for fut in as_completed(future_map):
|
||||||
|
inp, outp = future_map[fut]
|
||||||
|
try:
|
||||||
|
fut.result()
|
||||||
|
except Exception as e:
|
||||||
|
with print_lock:
|
||||||
|
print(f"[Error] Failed to convert {inp} → {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Convert OSM PNG tiles into LVGL-friendly .bin files (RGB565).",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-i", "--input",
|
||||||
|
required=True,
|
||||||
|
default=argparse.SUPPRESS, # hide '(default: None)'
|
||||||
|
help="Input root folder containing tiles in zoom/x/y.png structure",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o", "--output",
|
||||||
|
required=True,
|
||||||
|
default=argparse.SUPPRESS, # hide '(default: None)'
|
||||||
|
help="Output root folder where .bin tiles will be written",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-j", "--jobs",
|
||||||
|
type=int,
|
||||||
|
default=os.cpu_count(),
|
||||||
|
help="Number of worker threads",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-f", "--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Rebuild even if output file already exists",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Basic checks
|
||||||
|
if not os.path.isdir(args.input):
|
||||||
|
parser.error(f"Input folder not found or not a directory: {args.input}")
|
||||||
|
os.makedirs(args.output, exist_ok=True)
|
||||||
|
|
||||||
|
# Apply CLI values
|
||||||
|
INPUT_ROOT = args.input
|
||||||
|
OUTPUT_ROOT = args.output
|
||||||
|
|
||||||
|
convert_all_tiles(jobs=max(1, args.jobs), force=args.force)
|
||||||
Reference in New Issue
Block a user