339 lines
12 KiB
Python
339 lines
12 KiB
Python
![]() |
#!/usr/bin/python
|
||
|
# -*- coding: utf-8 -*-
|
||
|
from dataclasses import dataclass, field
|
||
|
from PIL import Image
|
||
|
from pathlib import Path
|
||
|
import json
|
||
|
from math import sqrt
|
||
|
from typing import List, Tuple, Union
|
||
|
|
||
|
|
||
|
HBITS = 20
|
||
|
VBITS = 19
|
||
|
h_mask = 2 ** HBITS - 1
|
||
|
v_mask = 2 ** VBITS - 1
|
||
|
h_limit = 2 ** (HBITS - 1) - 1
|
||
|
h_half = 2 ** (HBITS - 2)
|
||
|
v_limit = 2 ** (VBITS - 1) - 1
|
||
|
v_half = 2 ** (VBITS - 2)
|
||
|
|
||
|
# print(f'{h_mask=:_X} {h_limit=:_X} {h_half=:_X} ')
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class Point2D:
|
||
|
x: float = 0.0
|
||
|
y: float = 0.0
|
||
|
z: float = 0.0
|
||
|
|
||
|
def distance_to(self, p) -> float:
|
||
|
dx = self.x - p.x
|
||
|
dy = self.y - p.y
|
||
|
dz = self.z - p.z
|
||
|
return sqrt(dx * dx + dy * dy + dz * dz)
|
||
|
|
||
|
def make_key(self) -> int:
|
||
|
"""Returns the integer hash key"""
|
||
|
# coordinate considerate fino al centimetro
|
||
|
# xx = int(500000 + round(100.0 * self.x, 2))
|
||
|
# yy = int(500000 + round(100.0 * self.y, 2))
|
||
|
# return f'{xx + 1000000 * yy}'
|
||
|
xi = int(round(self.x * 100.0, 0)) + h_half
|
||
|
yi = int(round(self.y * 100.0, 0)) + h_half
|
||
|
zi = int(round(self.z * 100.0, 0)) + v_half
|
||
|
if xi < 0:
|
||
|
xi = 0
|
||
|
if yi < 0:
|
||
|
yi = 0
|
||
|
if zi < 0:
|
||
|
zi = 0
|
||
|
if xi > h_limit:
|
||
|
xi = h_limit
|
||
|
if yi > h_limit:
|
||
|
yi = h_limit
|
||
|
if zi > v_limit:
|
||
|
zi = v_limit
|
||
|
xi += yi << HBITS
|
||
|
xi += zi << (HBITS + HBITS)
|
||
|
return xi
|
||
|
|
||
|
def from_key(self, key):
|
||
|
"""collects x, y and z value from a hash key. the key should be an integer or a string representation of it"""
|
||
|
if not isinstance(key, int):
|
||
|
key = int(key) # let's say they gave you a string...
|
||
|
x_bits = key & h_mask
|
||
|
x_bits -= h_half
|
||
|
self.x = x_bits / 100.00
|
||
|
key >>= HBITS
|
||
|
y_bits = key & h_mask
|
||
|
y_bits -= h_half
|
||
|
self.y = y_bits / 100.00
|
||
|
key >>= HBITS
|
||
|
z_bits = key & v_mask
|
||
|
z_bits -= v_half
|
||
|
self.z = z_bits / 100.00
|
||
|
|
||
|
def to_str_array(self) -> Tuple[str, str, str]:
|
||
|
"""returns an array of three strings with the x, y, z values printed as strings with 2 decimal places"""
|
||
|
return f'{self.x:.2f}', f'{self.y:.2f}', f'{self.z:.2f}'
|
||
|
|
||
|
def from_array(self, arr: Union[Tuple, List, dict]):
|
||
|
"""Tries to parse the input array to collect the x, y and z value default value for z is 0.0"""
|
||
|
if len(arr) < 2:
|
||
|
raise ValueError(f'Minimum arr len is 2')
|
||
|
if isinstance(arr, dict):
|
||
|
if 'x' in arr:
|
||
|
self.x = arr['x']
|
||
|
if 'y' in arr:
|
||
|
self.y = arr['y']
|
||
|
if 'z' in arr:
|
||
|
self.z = arr['z']
|
||
|
elif isinstance(arr, list) or isinstance(arr, tuple):
|
||
|
self.x = round(float(arr[0]), 2)
|
||
|
self.y = round(float(arr[1]), 2)
|
||
|
if len(arr) >= 3:
|
||
|
self.z = round(float(arr[2]), 2)
|
||
|
|
||
|
def round_me(self):
|
||
|
self.x = round(self.x, 2)
|
||
|
self.y = round(self.y, 2)
|
||
|
self.z = round(self.z, 2)
|
||
|
|
||
|
def __hash__(self):
|
||
|
return hash((self.x, self.y, self.z))
|
||
|
|
||
|
def to_tuple(self):
|
||
|
return self.x, self.y, self.z
|
||
|
|
||
|
|
||
|
class ScaledMap:
|
||
|
"""Class representing a map that comes along with scaling values between realworld and pixels"""
|
||
|
|
||
|
def __init__(self, filename=None):
|
||
|
"""
|
||
|
:param filename: Filename of the _data file
|
||
|
"""
|
||
|
if filename is None:
|
||
|
raise ValueError('You MUST supply a _data file')
|
||
|
self.pr1 = None
|
||
|
self.pm1 = None
|
||
|
self.pr2 = None
|
||
|
self.pm2 = None
|
||
|
self.x_scale = 1.0
|
||
|
self.y_scale = 1.0
|
||
|
self.y_is_up = True
|
||
|
self.filename = filename
|
||
|
self.width, self.height = 0, 0
|
||
|
p = Path(filename)
|
||
|
self.full_filename = p
|
||
|
self.folder = p.parent
|
||
|
self.suffix = p.suffix
|
||
|
is_data = self.suffix.endswith('_data')
|
||
|
is_jd = self.suffix.endswith('_jd')
|
||
|
self.image_file = ''
|
||
|
if not is_data and not is_jd:
|
||
|
raise ValueError('Supplied file suffix MUST end with "_data" or "_jd"')
|
||
|
print(self.image_file, self.width, self.height)
|
||
|
if is_data:
|
||
|
self.image_file = p.parent / f'{p.stem}{p.suffix[:-5]}'
|
||
|
with Image.open(self.image_file) as img:
|
||
|
self.width, self.height = img.size
|
||
|
self.load_from_file(filename)
|
||
|
else:
|
||
|
self.load_from_jd(filename)
|
||
|
|
||
|
def __str__(self):
|
||
|
s = [f'{k} = {v}' for k, v in self.__dict__.items()]
|
||
|
return '\n'.join(s)
|
||
|
|
||
|
def adj_scales(self):
|
||
|
self.x_scale = (self.pr2.x - self.pr1.x) / (self.pm2.x - self.pm1.x)
|
||
|
self.y_scale = (self.pr2.y - self.pr1.y) / (self.pm2.y - self.pm1.y)
|
||
|
|
||
|
def load_from_file(self, infile):
|
||
|
n = 0
|
||
|
with open(infile, 'r') as fin:
|
||
|
for raw_line in fin.readlines():
|
||
|
line = raw_line.strip()
|
||
|
if line.startswith('#'):
|
||
|
continue
|
||
|
if ' ' in line:
|
||
|
fields = line.split(' ')
|
||
|
if len(fields) == 4:
|
||
|
vals = [float(v) for v in fields]
|
||
|
if n == 0:
|
||
|
self.pm1 = Point2D(vals[0], vals[1])
|
||
|
self.pr1 = Point2D(vals[2], vals[3])
|
||
|
n = 1
|
||
|
elif n == 1:
|
||
|
self.pm2 = Point2D(vals[0], vals[1])
|
||
|
self.pr2 = Point2D(vals[2], vals[3])
|
||
|
n = 2
|
||
|
elif len(fields) == 2:
|
||
|
if 'Y' == fields[0].upper():
|
||
|
self.y_is_up = fields[1].upper() in ('UP', 'UPWARD', 'NORD')
|
||
|
if not self.y_is_up:
|
||
|
self.pm1.y = self.height - self.pm1.y
|
||
|
self.pm2.y = self.height - self.pm2.y
|
||
|
self.y_is_up = True
|
||
|
self.adj_scales()
|
||
|
|
||
|
def load_from_jd(self, infile):
|
||
|
n = 0
|
||
|
self.filename = infile
|
||
|
with open(infile, 'r') as fin:
|
||
|
obj = json.load(fin)
|
||
|
self.image_file = obj.get('imagefile', None)
|
||
|
h = obj.get('height', None)
|
||
|
w = obj.get('width', None)
|
||
|
with Image.open(self.image_file) as img:
|
||
|
self.width, self.height = img.size
|
||
|
if h != self.height:
|
||
|
raise ValueError(f'File:{infile} image height={h} realimage({self.image_file}) height={self.height}')
|
||
|
if w != self.width:
|
||
|
raise ValueError(f'File:{infile} image width={w} realimage({self.image_file}) height={self.width}')
|
||
|
self.y_is_up = obj.get('y_is_up', True)
|
||
|
pr1 = obj.get('pr1', None)
|
||
|
pm1 = obj.get('pm1', None)
|
||
|
pr2 = obj.get('pr2', None)
|
||
|
pm2 = obj.get('pm2', None)
|
||
|
self.pr1 = Point2D(pr1['x'], pr1['y'])
|
||
|
self.pm1 = Point2D(pm1['x'], pm1['y'])
|
||
|
self.pr2 = Point2D(pr2['x'], pr2['y'])
|
||
|
self.pm2 = Point2D(pm2['x'], pm2['y'])
|
||
|
if not self.y_is_up:
|
||
|
self.pm1.y = self.height - self.pm1.y
|
||
|
self.pm2.y = self.height - self.pm2.y
|
||
|
self.y_is_up = True
|
||
|
self.adj_scales()
|
||
|
sx_ratio = self.x_scale / obj['scale_x']
|
||
|
sy_ratio = self.y_scale / obj['scale_y']
|
||
|
print(f'Scales ratio: {sx_ratio} {sy_ratio}')
|
||
|
|
||
|
def map_data_to_file(self, outfile):
|
||
|
with open(outfile, 'w', encoding='utf-8', newline='\n') as fout:
|
||
|
fout.write('# xmap ymap xreal yreal\nY UP\n')
|
||
|
fout.write(f'IMAGE {self.image_file}\n')
|
||
|
pr = self.get_real_coord(Point2D(0.0, 0.0))
|
||
|
fout.write(f'0.0 0.0 {pr.x} {pr.y}\n')
|
||
|
pr = self.get_real_coord(Point2D(self.width, self.height))
|
||
|
fout.write(f'{self.width} {self.height} {pr.x} {pr.y}\n')
|
||
|
|
||
|
def get_real_coord(self, map_point: Point2D, round_it=True) -> Point2D:
|
||
|
"""
|
||
|
Restituisce un Point2D in coordinate reali (metri)
|
||
|
:param map_point: Point2D in coordinate pixel/mappa
|
||
|
:param round_it: Se Vero, i valori in metri vengono arrotondati a 2 decimali (aka cm)
|
||
|
:return: Point2D in coordinate reali (metri)
|
||
|
"""
|
||
|
if round_it:
|
||
|
return Point2D(round(self.pr1.x + self.x_scale * (map_point.x - self.pm1.x), 2),
|
||
|
round(self.pr1.y + self.y_scale * (map_point.y - self.pm1.y), 2),
|
||
|
map_point.z,
|
||
|
)
|
||
|
else:
|
||
|
return Point2D(self.pr1.x + self.x_scale * (map_point.x - self.pm1.x),
|
||
|
self.pr1.y + self.y_scale * (map_point.y - self.pm1.y),
|
||
|
map_point.z,
|
||
|
)
|
||
|
|
||
|
def get_map_coord(self, real_point: Point2D, round_it=True) -> Point2D:
|
||
|
"""
|
||
|
Restituisce un Point2D in coordinate pixel/mappa
|
||
|
:param real_point: Point2D in coordinate reali (m)
|
||
|
:param round_it: Se Vero restituisce coordinate intere
|
||
|
:return: Point2D in coordinate pixel/mappa
|
||
|
"""
|
||
|
if round_it:
|
||
|
return Point2D(round(self.pm1.x + (real_point.x - self.pr1.x) / self.x_scale),
|
||
|
round(self.pm1.y + (real_point.y - self.pr1.y) / self.y_scale),
|
||
|
real_point.z,
|
||
|
)
|
||
|
else:
|
||
|
return Point2D(self.pm1.x + (real_point.x - self.pr1.x) / self.x_scale,
|
||
|
self.pm1.y + (real_point.y - self.pr1.y) / self.y_scale,
|
||
|
real_point.z,
|
||
|
)
|
||
|
|
||
|
def to_js_code(self):
|
||
|
"""Returns javaScript code to deal with the map coordinates"""
|
||
|
ll = self.get_real_coord(Point2D(0.0, 0.0))
|
||
|
ur = self.get_real_coord(Point2D(self.width, self.height))
|
||
|
return """ const bounds = [L.latLng({lly}, {llx}), L.latLng({ury},{urx})]
|
||
|
const map = L.map('map', {{crs: L.CRS.Simple, layers: [], minZoom: -2, zoomDelta: 0.5}});
|
||
|
map.fitBounds(bounds);
|
||
|
L.DomUtil.addClass(map._container,'crosshair-cursor-enabled');
|
||
|
const image = L.imageOverlay('{img_file}', bounds).addTo(map);""".format(llx=ll.x, lly=ll.y, urx=ur.x, ury=ur.y, img_file=self.image_file)
|
||
|
|
||
|
def to_json(self):
|
||
|
return {
|
||
|
'name': "",
|
||
|
'imagefile': str(self.image_file),
|
||
|
'width': self.width,
|
||
|
'height': self.height,
|
||
|
'y_is_up': self.y_is_up,
|
||
|
'pr1': {'x': self.pr1.x, 'y': self.pr1.y},
|
||
|
'pm1': {'x': self.pm1.x, 'y': self.pm1.y},
|
||
|
'pr2': {'x': self.pr2.x, 'y': self.pr2.y},
|
||
|
'pm2': {'x': self.pm2.x, 'y': self.pm2.y},
|
||
|
'scale_x': self.x_scale,
|
||
|
'scale_y': self.y_scale,
|
||
|
}
|
||
|
|
||
|
|
||
|
def main():
|
||
|
# FILE = 'Mozz_map.png_data'
|
||
|
# # FILE = 'Mappe-ModelPPP.png_data'
|
||
|
# my_map = ScaledMap(FILE)
|
||
|
# # pm = moz.get_map_coord(Point2D())
|
||
|
# # print(pm)
|
||
|
# # print(moz.get_real_coord(pm))
|
||
|
# # print('-------------')
|
||
|
# # pr = my_map.get_real_coord(Point2D(0.0, 0.0))
|
||
|
# # print(f'0.0 0.0 {pr}')
|
||
|
# # pr = my_map.get_real_coord(Point2D(my_map.width, my_map.height))
|
||
|
# # print(f'{my_map.width} {my_map.height} {pr}')
|
||
|
# print(my_map.to_js_code())
|
||
|
# my_map.map_data_to_file('tt.txt')
|
||
|
# obj = my_map.to_json()
|
||
|
# with open('t._jd', 'w', encoding='utf-8', newline='\n') as fout:
|
||
|
# json.dump(obj, fout, indent=4)
|
||
|
# print(obj)
|
||
|
# new_img = ScaledMap('t._jd')
|
||
|
# print(json.dumps(new_img.to_json(), indent=4))
|
||
|
# new_img = ScaledMap('fpm1_manual._jd')
|
||
|
|
||
|
# new_img = ScaledMap('f2._jd')
|
||
|
# obj = new_img.to_json()
|
||
|
# print(obj)
|
||
|
# with open('FPM2_web._jd', 'w', encoding='utf-8', newline='\n') as fout:
|
||
|
# json.dump(obj, fout, indent=4)
|
||
|
# print(new_img.to_js_code())
|
||
|
|
||
|
# new_img = ScaledMap('FPM1_web._jd')
|
||
|
# obj = new_img.to_json()
|
||
|
# print(obj)
|
||
|
# # with open('FPM2_web._jd', 'w', encoding='utf-8', newline='\n') as fout:
|
||
|
# # json.dump(obj, fout, indent=4)
|
||
|
# print(new_img.to_js_code())
|
||
|
def check_me(x=0, y=0, z=0):
|
||
|
p = Point2D(x, y, z)
|
||
|
key = p.make_key()
|
||
|
print(f'Input {p} {key:_X} {key}')
|
||
|
pr = Point2D()
|
||
|
pr.from_key(key)
|
||
|
key_r = pr.make_key()
|
||
|
print(f'Output:{pr} {key_r:_X} {key_r}')
|
||
|
print()
|
||
|
return p
|
||
|
|
||
|
p = check_me()
|
||
|
p = check_me(100, 200, 500)
|
||
|
|
||
|
p = check_me(100, 200, 1000)
|
||
|
print(f'{p} {p.make_key():_X} {p.make_key()} {p.to_str_array()}')
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
main()
|