Kartenservice erstellt. Erster Test in der Ankerseite
This commit is contained in:
parent
cd04a5560d
commit
1661c81dfa
|
|
@ -7,6 +7,7 @@ import os
|
|||
from web import WebInterface
|
||||
from tracker import Tracker
|
||||
from navtex import NAVTEX
|
||||
from mapservice import MapService
|
||||
|
||||
class AppData():
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ class AppData():
|
|||
else:
|
||||
self.web = None
|
||||
self.track = Tracker(logger, cfg)
|
||||
self.mapsrv = MapService(logger, cfg)
|
||||
self.frontend = None
|
||||
self.bv_lat = None
|
||||
self.bv_lon = None
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -33,6 +33,7 @@ import math
|
|||
import time
|
||||
from cfgmenu import Menu
|
||||
from .page import Page
|
||||
from PIL import Image
|
||||
|
||||
class Anchor(Page):
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ class Anchor(Page):
|
|||
self.buttonlabel[5] = '' # ALARM erst möglich wenn der Anker unten ist
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -68,6 +69,10 @@ class Anchor(Page):
|
|||
self.alarm = False # Alarm ist ausgelöst und aktiv
|
||||
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
|
||||
self._menu = Menu("Options", 20, 80)
|
||||
self._menu.setItemDimension(120, 20)
|
||||
|
|
@ -179,6 +184,12 @@ class Anchor(Page):
|
|||
|
||||
# self.anchor_lat =
|
||||
|
||||
# Seekarte als Hintergrundbild
|
||||
ctx.save()
|
||||
ctx.set_source_surface(self.bgimage, (400 - 260) // 2, 20)
|
||||
ctx.paint()
|
||||
ctx.restore()
|
||||
|
||||
# Name
|
||||
ctx.select_font_face("Ubuntu", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
||||
ctx.set_font_size(20)
|
||||
|
|
|
|||
Loading…
Reference in New Issue