#!/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()