Kartenservice erstellt. Erster Test in der Ankerseite

This commit is contained in:
Thomas Hooge 2025-10-26 18:45:49 +01:00
parent cd04a5560d
commit 1661c81dfa
3 changed files with 194 additions and 1 deletions

View File

@ -7,6 +7,7 @@ import os
from web import WebInterface from web import WebInterface
from tracker import Tracker from tracker import Tracker
from navtex import NAVTEX from navtex import NAVTEX
from mapservice import MapService
class AppData(): class AppData():
@ -22,6 +23,7 @@ class AppData():
else: else:
self.web = None self.web = None
self.track = Tracker(logger, cfg) self.track = Tracker(logger, cfg)
self.mapsrv = MapService(logger, cfg)
self.frontend = None self.frontend = None
self.bv_lat = None self.bv_lat = None
self.bv_lon = None self.bv_lon = None

180
mapservice.py Normal file
View File

@ -0,0 +1,180 @@
"""
Map access inspired by Norbert Walter
"""
import os
import math
import http.client
import ssl
from io import BytesIO
from PIL import Image, ImageDraw
import cairo
class MapService():
def __init__(self, logger, cfg):
self.cachepath = os.path.join(cfg['histpath'], "tilecache")
self.lat = 53.56938345759218
self.lon = 9.679658234303275
self.dither_type = Image.FLOYDSTEINBERG
self.width = 260
self.height = 260
self.zoom_level = 15
self.debug = False
def set_output_size(self, width, height):
self.width = width
self.height = height
def web_get_tile(self, domain, path):
ssl_context = ssl.create_default_context()
conn = http.client.HTTPSConnection(domain, 443, context=ssl_context)
headers = {
#"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0"
"User-Agent": "OBP/1.0 (X11; Linux x86_64; rv:0.1) OBP60v/0.1"
}
data = None
try:
conn.request("GET", path, headers=headers)
response = conn.getresponse()
if response.status == 200:
data = response.read()
else:
print(f"Error: {response.status}")
except http.client.HTTPException as e:
print(f"HTTP error occurred: {e}")
except ssl.SSLError as ssl_error:
print(f"SSL error occurred: {ssl_error}")
finally:
conn.close()
return data
def latlon_to_xyz(self, lat, lon, zoom):
"""
Converts geographic coordinates (Lat, Lon) to X, Y coordinates for the tiling system
and returns the pixel offset of the exact position in the tile.
"""
# Calculate the X tile coordinate
x_tile = (lon + 180.0) / 360.0 * (2 ** zoom)
# Calculate the Y tile coordinate
lat_rad = math.radians(lat)
y_tile = (1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * (2 ** zoom)
# Integer part is the tile coordinates
x = int(x_tile)
y = int(y_tile)
# Decimal part determines the offset within the tile
x_offset = int((x_tile - x) * 256) # Each tile is 256x256 pixels
y_offset = int((y_tile - y) * 256)
return x, y, x_offset, y_offset
def fetch_osm_tile(self, x, y, zoom):
tile_key = f"{zoom}/{x}/{y}.png"
cache_dir = os.path.join(self.cachepath, str(zoom), str(x))
os.makedirs(cache_dir, exist_ok=True) # Create the directory if it doesn't exist
tile_path = os.path.join(cache_dir, f"{y}.png")
# Check if the tile exists in the disk cache
if os.path.exists(tile_path):
#print(f"Tile {x}, {y} loaded from disk cache.")
with open(tile_path, 'rb') as f:
tile_data = f.read()
#ram_cache.set(cache_key, tile_data) # Load into RAM cache
return Image.open(tile_path)
data = self.web_get_tile("freenauticalchart.net", f"/qmap-de/{zoom}/{x}/{y}.png")
if data:
tile = Image.open(BytesIO(data))
tile.save(tile_path)
return tile
else:
return Image.new('RGB', (256, 256), (200, 200, 200)) # Fallback image
def draw_cross(self, draw, x_offset, y_offset):
line_length = 10 # Length of cross lines
line_color = (255, 0, 0) # Red
draw.line((x_offset - line_length, y_offset, x_offset + line_length, y_offset), fill=line_color, width=2)
draw.line((x_offset, y_offset - line_length, x_offset, y_offset + line_length), fill=line_color, width=2)
def draw_tile_borders(self, draw, tile_x, tile_y):
"""
Draws a black line around each tile with a pixel width.
"""
top_left_x = tile_x * 256
top_left_y = tile_y * 256
bottom_right_x = top_left_x + 256
bottom_right_y = top_left_y + 256
draw.rectangle((top_left_x, top_left_y, bottom_right_x - 1, bottom_right_y - 1), outline="black", width=1)
def stitch_tiles(self, lat, lon, zoom, debug=False):
"""
Loads the required tiles and stitches them into one image,
and then crop that image so that lat/lon ist centered
"""
# Convert geo-coordinates to tile coordinates and offset
x_tile, y_tile, x_offset, y_offset = self.latlon_to_xyz(lat, lon, zoom)
# No rotation, north up: calculation of needed tiles
num_tiles_x = int(self.width // 256) + 2
num_tiles_y = int(self.height // 256) + 2
central_tile_x = num_tiles_x // 2
central_tile_y = num_tiles_y // 2
center_x = central_tile_x * 256 + x_offset
center_y = central_tile_y * 256 + y_offset
# Create an empty image for the final mosaic
combined_image = Image.new('RGB', (num_tiles_x * 256, num_tiles_y * 256))
draw = ImageDraw.Draw(combined_image)
# Download and stitch the tiles
x_tile -= num_tiles_x // 2
y_tile -= num_tiles_y // 2
for i in range(num_tiles_x):
for j in range(num_tiles_y):
tile = self.fetch_osm_tile(x_tile + i, y_tile + j, zoom)
combined_image.paste(tile, (i * 256, j * 256))
if debug == True:
# Draw the black line around each tile
self.draw_tile_borders(draw, i, j)
if debug:
# Draw a cross on the central tile at the offset position
self.draw_cross(draw, center_x, center_y)
# Determine the crop area
new_left = int(center_x - self.width // 2)
new_top = int(center_y - self.height // 2)
new_right = new_left + self.width
new_bottom = new_top + self.height
return combined_image.crop((new_left, new_top, new_right, new_bottom))
def get_round_bwmap(self, lat, lon, zoom, debug=False):
# Rundes s/w Bild erzeugen z.B. für Ankerkreis
image = self.stitch_tiles(lat, lon, zoom, debug)
mask = Image.new('1', (self.width, self.height), 0)
draw = ImageDraw.Draw(mask)
radius = min(self.width, self.height) // 2
center_x = self.width // 2
center_y = self.height // 2
bounding_box = (center_x - radius, center_y - radius, center_x + radius, center_y + radius)
draw.ellipse(bounding_box, fill=255)
image.putalpha(mask)
roundimage = Image.new('RGB', (self.width, self.height), (255, 255, 255))
roundimage.paste(image, mask=image.split()[3])
return roundimage.convert('1', dither=self.dither_type)
def get_round_bwmap_cairo(self, lat, lon, zoom, bgcolor=(255,255,255), debug=False):
image = self.get_round_bwmap(lat, lon, zoom, debug).convert("RGBA")
r, g, b, a = image.split()
r = r.point(lambda p: bgcolor[0] if p == 255 else p)
g = g.point(lambda p: bgcolor[1] if p == 255 else p)
b = b.point(lambda p: bgcolor[2] if p == 255 else p)
image = Image.merge("RGBA", (r, g, b, a))
data = bytearray(image.tobytes())
return cairo.ImageSurface.create_for_data(data, cairo.FORMAT_ARGB32, self.width, self.height)

View File

@ -33,6 +33,7 @@ import math
import time import time
from cfgmenu import Menu from cfgmenu import Menu
from .page import Page from .page import Page
from PIL import Image
class Anchor(Page): class Anchor(Page):
@ -44,7 +45,7 @@ class Anchor(Page):
self.buttonlabel[5] = '' # ALARM erst möglich wenn der Anker unten ist self.buttonlabel[5] = '' # ALARM erst möglich wenn der Anker unten ist
self.mode = 'N' # (N)ormal, (C)onfiguration self.mode = 'N' # (N)ormal, (C)onfiguration
self.scale = 50 # Radius of display circle in meter self.scale = 425 # Radius of display circle in meter
self._bd = boatdata self._bd = boatdata
@ -68,6 +69,10 @@ class Anchor(Page):
self.alarm = False # Alarm ist ausgelöst und aktiv self.alarm = False # Alarm ist ausgelöst und aktiv
self.wind_angle = -1 self.wind_angle = -1
# Seekarte, mit Hintergrund wie das Display
self.app.mapsrv.set_output_size(260, 260)
self.bgimage = self.app.mapsrv.get_round_bwmap_cairo(53.56938345759218, 9.679658234303275, 15, (220, 220, 220))
# Menüsteuerung für Konfiguration # Menüsteuerung für Konfiguration
self._menu = Menu("Options", 20, 80) self._menu = Menu("Options", 20, 80)
self._menu.setItemDimension(120, 20) self._menu.setItemDimension(120, 20)
@ -179,6 +184,12 @@ class Anchor(Page):
# self.anchor_lat = # self.anchor_lat =
# Seekarte als Hintergrundbild
ctx.save()
ctx.set_source_surface(self.bgimage, (400 - 260) // 2, 20)
ctx.paint()
ctx.restore()
# Name # Name
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD) ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
ctx.set_font_size(20) ctx.set_font_size(20)