2020-06-09 14:07:47 -04:00
|
|
|
"""
|
|
|
|
Geometry helper
|
|
|
|
SPDX - License - Identifier: LGPL - 3.0 - or -later
|
2022-04-08 09:35:33 -04:00
|
|
|
Copyright © 2022 Concordia CERC group
|
|
|
|
Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
|
|
|
|
Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca
|
2020-06-09 14:07:47 -04:00
|
|
|
"""
|
2020-05-19 17:00:15 -04:00
|
|
|
import math
|
2023-04-13 09:51:07 -04:00
|
|
|
from pathlib import Path
|
2023-05-31 13:51:35 -04:00
|
|
|
from typing import Dict
|
2023-03-17 16:32:54 -04:00
|
|
|
|
2023-04-13 09:51:07 -04:00
|
|
|
from PIL import Image
|
2020-06-09 11:34:12 -04:00
|
|
|
from trimesh import Trimesh
|
|
|
|
from trimesh import intersections
|
2023-05-31 13:51:35 -04:00
|
|
|
import numpy as np
|
2023-03-10 14:09:41 -05:00
|
|
|
|
2023-01-24 10:51:50 -05:00
|
|
|
from hub.city_model_structure.attributes.polygon import Polygon
|
|
|
|
from hub.city_model_structure.attributes.polyhedron import Polyhedron
|
|
|
|
from hub.helpers.location import Location
|
2020-05-19 17:00:15 -04:00
|
|
|
|
2023-02-23 07:25:04 -05:00
|
|
|
|
2023-02-23 06:56:13 -05:00
|
|
|
class MapPoint:
|
2023-05-31 13:51:35 -04:00
|
|
|
"""
|
|
|
|
Map point class
|
|
|
|
"""
|
2023-02-23 06:56:13 -05:00
|
|
|
def __init__(self, x, y):
|
2023-02-23 15:35:13 -05:00
|
|
|
self._x = int(x)
|
|
|
|
self._y = int(y)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def x(self):
|
2023-05-18 16:15:57 -04:00
|
|
|
"""
|
|
|
|
Get X Coordinate
|
|
|
|
"""
|
2023-02-23 15:35:13 -05:00
|
|
|
return self._x
|
|
|
|
|
|
|
|
@property
|
|
|
|
def y(self):
|
2023-05-18 16:15:57 -04:00
|
|
|
"""
|
|
|
|
Get Y Coordinate
|
|
|
|
"""
|
2023-02-23 15:35:13 -05:00
|
|
|
return self._y
|
2023-02-23 06:56:13 -05:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return f'({self.x}, {self.y})'
|
2020-05-19 17:00:15 -04:00
|
|
|
|
2023-02-23 15:35:13 -05:00
|
|
|
def __len__(self):
|
|
|
|
return 1
|
|
|
|
|
|
|
|
def __getitem__(self, index):
|
|
|
|
if index == 0:
|
|
|
|
return self._x
|
2023-05-31 13:51:35 -04:00
|
|
|
if index == 1:
|
2023-02-23 15:35:13 -05:00
|
|
|
return self._y
|
2023-05-31 13:51:35 -04:00
|
|
|
raise IndexError('Index error')
|
2023-02-23 15:35:13 -05:00
|
|
|
|
2023-02-23 07:25:04 -05:00
|
|
|
|
2020-06-11 16:22:58 -04:00
|
|
|
class GeometryHelper:
|
2020-06-11 15:45:11 -04:00
|
|
|
"""
|
|
|
|
Geometry helper class
|
|
|
|
"""
|
2023-02-23 07:25:04 -05:00
|
|
|
# todo: complete dictionary
|
2022-11-24 17:58:45 -05:00
|
|
|
srs_transformations = {
|
|
|
|
'urn:adv:crs:ETRS89_UTM32*DE_DHHN92_NH': 'epsg:25832'
|
|
|
|
}
|
2021-01-07 16:16:48 -05:00
|
|
|
|
2023-04-21 10:31:55 -04:00
|
|
|
@staticmethod
|
|
|
|
def factor():
|
2023-05-18 16:15:57 -04:00
|
|
|
"""
|
|
|
|
Set minimap resolution
|
|
|
|
:return: None
|
|
|
|
"""
|
2023-04-21 12:57:13 -04:00
|
|
|
return 0.5
|
2023-04-21 10:31:55 -04:00
|
|
|
|
2021-01-13 16:41:45 -05:00
|
|
|
def __init__(self, delta=0, area_delta=0):
|
2020-05-19 17:00:15 -04:00
|
|
|
self._delta = delta
|
2020-12-02 11:56:33 -05:00
|
|
|
self._area_delta = area_delta
|
2020-05-19 17:00:15 -04:00
|
|
|
|
2020-06-29 17:24:05 -04:00
|
|
|
@staticmethod
|
2023-04-21 12:48:50 -04:00
|
|
|
def coordinate_to_map_point(coordinate, city):
|
2023-05-18 16:15:57 -04:00
|
|
|
"""
|
|
|
|
Transform a real world coordinate to a minimap one
|
|
|
|
:param coordinate: real world coordinate
|
|
|
|
:param city: current city
|
|
|
|
:return: None
|
|
|
|
"""
|
2023-04-21 10:31:55 -04:00
|
|
|
factor = GeometryHelper.factor()
|
2023-05-17 17:10:30 -04:00
|
|
|
return MapPoint(
|
|
|
|
((coordinate[0] - city.lower_corner[0]) * factor), ((coordinate[1] - city.lower_corner[1]) * factor)
|
|
|
|
)
|
2023-02-23 06:56:13 -05:00
|
|
|
|
|
|
|
@staticmethod
|
2023-05-18 16:15:57 -04:00
|
|
|
def city_mapping(city, building_names=None, plot=False) -> Dict:
|
2023-02-24 07:39:08 -05:00
|
|
|
"""
|
2023-05-18 16:15:57 -04:00
|
|
|
:param city: city to be mapped
|
|
|
|
:param building_names: list of building names to be mapped or None
|
|
|
|
:param plot: True if minimap image should be displayed
|
|
|
|
:return: shared_information dictionary
|
2023-02-24 07:39:08 -05:00
|
|
|
"""
|
2023-03-10 14:09:41 -05:00
|
|
|
lines_information = {}
|
2023-02-23 06:56:13 -05:00
|
|
|
if building_names is None:
|
|
|
|
building_names = [b.name for b in city.buildings]
|
2023-04-21 10:31:55 -04:00
|
|
|
factor = GeometryHelper.factor()
|
2023-04-21 12:48:50 -04:00
|
|
|
x = math.ceil((city.upper_corner[0] - city.lower_corner[0]) * factor) + 1
|
|
|
|
y = math.ceil((city.upper_corner[1] - city.lower_corner[1]) * factor) + 1
|
2023-03-10 14:09:41 -05:00
|
|
|
city_map = [['' for _ in range(y + 1)] for _ in range(x + 1)]
|
|
|
|
map_info = [[{} for _ in range(y + 1)] for _ in range(x + 1)]
|
2023-04-13 09:51:07 -04:00
|
|
|
img = Image.new('RGB', (x + 1, y + 1), "black") # create a new black image
|
|
|
|
city_image = img.load() # create the pixel map
|
2023-02-23 06:56:13 -05:00
|
|
|
for building_name in building_names:
|
|
|
|
building = city.city_object(building_name)
|
2023-02-24 07:39:08 -05:00
|
|
|
line = 0
|
2023-02-23 06:56:13 -05:00
|
|
|
for ground in building.grounds:
|
|
|
|
length = len(ground.perimeter_polygon.coordinates) - 1
|
|
|
|
for i, coordinate in enumerate(ground.perimeter_polygon.coordinates):
|
2023-04-13 09:51:07 -04:00
|
|
|
|
2023-02-23 15:35:13 -05:00
|
|
|
j = i + 1
|
2023-02-23 06:56:13 -05:00
|
|
|
if i == length:
|
|
|
|
j = 0
|
|
|
|
next_coordinate = ground.perimeter_polygon.coordinates[j]
|
2023-04-21 12:48:50 -04:00
|
|
|
distance = GeometryHelper.distance_between_points(coordinate, next_coordinate)
|
2023-04-22 09:33:06 -04:00
|
|
|
steps = int(distance * factor * 2)
|
2023-04-25 10:45:56 -04:00
|
|
|
if steps == 0:
|
|
|
|
continue
|
2023-04-22 09:33:06 -04:00
|
|
|
delta_x = (next_coordinate[0] - coordinate[0]) / steps
|
|
|
|
delta_y = (next_coordinate[1] - coordinate[1]) / steps
|
|
|
|
|
2023-04-21 12:48:50 -04:00
|
|
|
for k in range(0, steps):
|
|
|
|
new_coordinate = (coordinate[0] + (delta_x * k), coordinate[1] + (delta_y * k))
|
|
|
|
point = GeometryHelper.coordinate_to_map_point(new_coordinate, city)
|
|
|
|
x = point.x
|
|
|
|
y = point.y
|
2023-03-10 14:09:41 -05:00
|
|
|
if city_map[x][y] == '':
|
2023-02-23 06:56:13 -05:00
|
|
|
city_map[x][y] = building.name
|
2023-03-10 14:09:41 -05:00
|
|
|
map_info[x][y] = {
|
|
|
|
'line_start': (coordinate[0], coordinate[1]),
|
|
|
|
'line_end': (next_coordinate[0], next_coordinate[1]),
|
|
|
|
}
|
2023-04-13 09:51:07 -04:00
|
|
|
city_image[x, y] = (100, 0, 0)
|
2023-02-23 06:56:13 -05:00
|
|
|
elif city_map[x][y] != building.name:
|
|
|
|
neighbour = city.city_object(city_map[x][y])
|
2023-03-10 14:09:41 -05:00
|
|
|
neighbour_info = map_info[x][y]
|
|
|
|
|
|
|
|
# prepare the keys
|
2023-03-15 11:30:25 -04:00
|
|
|
neighbour_start_coordinate = f'{GeometryHelper.coordinate_to_map_point(neighbour_info["line_start"], city)}'
|
|
|
|
building_start_coordinate = f'{GeometryHelper.coordinate_to_map_point(coordinate, city)}'
|
2023-03-10 14:09:41 -05:00
|
|
|
neighbour_key = f'{neighbour.name}_{neighbour_start_coordinate}_{building_start_coordinate}'
|
|
|
|
building_key = f'{building.name}_{building_start_coordinate}_{neighbour_start_coordinate}'
|
|
|
|
|
|
|
|
# Add my neighbour info to my shared lines
|
2023-05-31 13:51:35 -04:00
|
|
|
if building.name in lines_information and neighbour_key in lines_information[building.name]:
|
2023-03-10 14:09:41 -05:00
|
|
|
shared_points = int(lines_information[building.name][neighbour_key]['shared_points'])
|
|
|
|
lines_information[building.name][neighbour_key]['shared_points'] = shared_points + 1
|
|
|
|
else:
|
2023-05-31 13:51:35 -04:00
|
|
|
if building.name not in lines_information:
|
2023-03-10 14:09:41 -05:00
|
|
|
lines_information[building.name] = {}
|
|
|
|
lines_information[building.name][neighbour_key] = {
|
|
|
|
'neighbour_name': neighbour.name,
|
|
|
|
'line_start': (coordinate[0], coordinate[1]),
|
|
|
|
'line_end': (next_coordinate[0], next_coordinate[1]),
|
|
|
|
'neighbour_line_start': neighbour_info['line_start'],
|
|
|
|
'neighbour_line_end': neighbour_info['line_end'],
|
2023-03-15 11:30:25 -04:00
|
|
|
'coordinate_start': f"{GeometryHelper.coordinate_to_map_point(coordinate, city)}",
|
|
|
|
'coordinate_end': f"{GeometryHelper.coordinate_to_map_point(next_coordinate, city)}",
|
|
|
|
'neighbour_start': f"{GeometryHelper.coordinate_to_map_point(neighbour_info['line_start'], city)}",
|
|
|
|
'neighbour_end': f"{GeometryHelper.coordinate_to_map_point(neighbour_info['line_end'], city)}",
|
2023-03-10 14:09:41 -05:00
|
|
|
'shared_points': 1
|
|
|
|
}
|
|
|
|
|
|
|
|
# Add my info to my neighbour shared lines
|
2023-05-31 13:51:35 -04:00
|
|
|
if neighbour.name in lines_information and building_key in lines_information[neighbour.name]:
|
2023-03-10 14:09:41 -05:00
|
|
|
shared_points = int(lines_information[neighbour.name][building_key]['shared_points'])
|
|
|
|
lines_information[neighbour.name][building_key]['shared_points'] = shared_points + 1
|
|
|
|
else:
|
2023-05-31 13:51:35 -04:00
|
|
|
if neighbour.name not in lines_information:
|
2023-03-10 14:09:41 -05:00
|
|
|
lines_information[neighbour.name] = {}
|
|
|
|
lines_information[neighbour.name][building_key] = {
|
|
|
|
'neighbour_name': building.name,
|
|
|
|
'line_start': neighbour_info['line_start'],
|
|
|
|
'line_end': neighbour_info['line_end'],
|
|
|
|
'neighbour_line_start': (coordinate[0], coordinate[1]),
|
|
|
|
'neighbour_line_end': (next_coordinate[0], next_coordinate[1]),
|
2023-03-15 11:30:25 -04:00
|
|
|
'neighbour_start': f"{GeometryHelper.coordinate_to_map_point(coordinate, city)}",
|
|
|
|
'neighbour_end': f"{GeometryHelper.coordinate_to_map_point(next_coordinate, city)}",
|
|
|
|
'coordinate_start': f"{GeometryHelper.coordinate_to_map_point(neighbour_info['line_start'], city)}",
|
|
|
|
'coordinate_end': f"{GeometryHelper.coordinate_to_map_point(neighbour_info['line_end'], city)}",
|
2023-03-10 14:09:41 -05:00
|
|
|
'shared_points': 1
|
|
|
|
}
|
|
|
|
|
2023-02-23 06:56:13 -05:00
|
|
|
if building.neighbours is None:
|
|
|
|
building.neighbours = [neighbour]
|
|
|
|
elif neighbour not in building.neighbours:
|
|
|
|
building.neighbours.append(neighbour)
|
|
|
|
if neighbour.neighbours is None:
|
|
|
|
neighbour.neighbours = [building]
|
|
|
|
elif building not in neighbour.neighbours:
|
|
|
|
neighbour.neighbours.append(building)
|
2023-02-24 07:39:08 -05:00
|
|
|
line += 1
|
2023-04-13 09:51:07 -04:00
|
|
|
|
|
|
|
if plot:
|
|
|
|
img.show()
|
|
|
|
return lines_information
|
|
|
|
|
|
|
|
@staticmethod
|
2021-03-31 14:17:53 -04:00
|
|
|
def segment_list_to_trimesh(lines) -> Trimesh:
|
2021-08-26 13:27:43 -04:00
|
|
|
"""
|
2023-05-18 16:15:57 -04:00
|
|
|
:param lines: lines
|
|
|
|
:return: Transform a list of segments into a Trimesh
|
2021-08-26 13:27:43 -04:00
|
|
|
"""
|
2023-02-10 05:42:57 -05:00
|
|
|
# todo: trimesh has a method for this
|
2021-03-30 17:57:28 -04:00
|
|
|
line_points = [lines[0][0], lines[0][1]]
|
|
|
|
lines.remove(lines[0])
|
|
|
|
while len(lines) > 1:
|
|
|
|
i = 0
|
|
|
|
for line in lines:
|
|
|
|
i += 1
|
|
|
|
if GeometryHelper.distance_between_points(line[0], line_points[len(line_points) - 1]) < 1e-8:
|
|
|
|
line_points.append(line[1])
|
|
|
|
lines.pop(i - 1)
|
|
|
|
break
|
2021-08-26 13:27:43 -04:00
|
|
|
if GeometryHelper.distance_between_points(line[1], line_points[len(line_points) - 1]) < 1e-8:
|
2021-03-30 17:57:28 -04:00
|
|
|
line_points.append(line[0])
|
|
|
|
lines.pop(i - 1)
|
|
|
|
break
|
2023-02-10 05:42:57 -05:00
|
|
|
polyhedron = Polyhedron(Polygon(line_points).triangles)
|
2021-03-31 14:17:53 -04:00
|
|
|
trimesh = Trimesh(polyhedron.vertices, polyhedron.faces)
|
|
|
|
return trimesh
|
2020-06-09 11:34:12 -04:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _merge_meshes(mesh1, mesh2):
|
|
|
|
v_1 = mesh1.vertices
|
|
|
|
f_1 = mesh1.faces
|
|
|
|
v_2 = mesh2.vertices
|
|
|
|
f_2 = mesh2.faces
|
|
|
|
length = len(v_1)
|
|
|
|
v_merge = np.concatenate((v_1, v_2))
|
|
|
|
f_merge = np.asarray(f_1)
|
|
|
|
|
|
|
|
for item in f_2:
|
|
|
|
point1 = item.item(0) + length
|
|
|
|
point2 = item.item(1) + length
|
|
|
|
point3 = item.item(2) + length
|
|
|
|
surface = np.asarray([point1, point2, point3])
|
|
|
|
f_merge = np.concatenate((f_merge, [surface]))
|
|
|
|
|
|
|
|
mesh_merge = Trimesh(vertices=v_merge, faces=f_merge)
|
2021-03-31 14:17:53 -04:00
|
|
|
mesh_merge.fix_normals()
|
2020-06-09 11:34:12 -04:00
|
|
|
|
|
|
|
return mesh_merge
|
|
|
|
|
|
|
|
@staticmethod
|
2021-03-31 14:17:53 -04:00
|
|
|
def divide_mesh_by_plane(trimesh, normal_plane, point_plane):
|
2020-06-11 15:45:11 -04:00
|
|
|
"""
|
|
|
|
Divide a mesh by a plane
|
2021-03-31 14:17:53 -04:00
|
|
|
:param trimesh: Trimesh
|
2020-06-11 15:45:11 -04:00
|
|
|
:param normal_plane: [x, y, z]
|
|
|
|
:param point_plane: [x, y, z]
|
|
|
|
:return: [Trimesh]
|
|
|
|
"""
|
2020-06-09 11:34:12 -04:00
|
|
|
# The first mesh returns the positive side of the plane and the second the negative side.
|
2023-05-17 17:10:30 -04:00
|
|
|
# If the plane does not divide the mesh (i.e. it does not touch it, or it is coplanar with one or more faces),
|
2020-06-09 11:34:12 -04:00
|
|
|
# then it returns only the original mesh.
|
2021-03-31 14:17:53 -04:00
|
|
|
# todo: review split method in https://github.com/mikedh/trimesh/issues/235,
|
|
|
|
# once triangulate_polygon in Polygon class is solved
|
|
|
|
|
2020-06-09 11:34:12 -04:00
|
|
|
normal_plane_opp = [None] * len(normal_plane)
|
2023-05-31 13:51:35 -04:00
|
|
|
for index, normal in enumerate(normal_plane):
|
|
|
|
normal_plane_opp[index] = - normal
|
2020-06-09 11:34:12 -04:00
|
|
|
|
2021-03-31 14:17:53 -04:00
|
|
|
section_1 = intersections.slice_mesh_plane(trimesh, normal_plane, point_plane)
|
|
|
|
if section_1 is None:
|
|
|
|
return [trimesh]
|
|
|
|
lines = list(intersections.mesh_plane(trimesh, normal_plane, point_plane))
|
|
|
|
cap = GeometryHelper.segment_list_to_trimesh(lines)
|
|
|
|
trimesh_1 = GeometryHelper._merge_meshes(section_1, cap)
|
|
|
|
|
|
|
|
section_2 = intersections.slice_mesh_plane(trimesh, normal_plane_opp, point_plane)
|
|
|
|
if section_2 is None:
|
|
|
|
return [trimesh_1]
|
|
|
|
trimesh_2 = GeometryHelper._merge_meshes(section_2, cap)
|
|
|
|
|
|
|
|
return [trimesh_1, trimesh_2]
|
2020-06-22 13:26:50 -04:00
|
|
|
|
2020-06-26 14:34:37 -04:00
|
|
|
@staticmethod
|
2021-08-26 13:27:43 -04:00
|
|
|
def get_location(latitude, longitude) -> Location:
|
|
|
|
"""
|
|
|
|
Get Location from latitude and longitude
|
2023-05-18 16:15:57 -04:00
|
|
|
:param latitude: Latitude
|
|
|
|
:param longitude: Longitude
|
|
|
|
:return: Location
|
2021-08-26 13:27:43 -04:00
|
|
|
"""
|
2023-04-13 09:51:07 -04:00
|
|
|
_data_path = Path(Path(__file__).parent.parent / 'data/geolocation/cities15000.txt').resolve()
|
|
|
|
latitude = float(latitude)
|
|
|
|
longitude = float(longitude)
|
|
|
|
distance = math.inf
|
|
|
|
country = 'Unknown'
|
2021-08-26 13:27:43 -04:00
|
|
|
city = 'Unknown'
|
2023-05-31 13:51:35 -04:00
|
|
|
with open(_data_path, 'r', encoding='utf-8') as file:
|
|
|
|
for _, line in enumerate(file):
|
2023-04-13 09:51:07 -04:00
|
|
|
fields = line.split('\t')
|
|
|
|
file_city_name = fields[2]
|
|
|
|
file_latitude = float(fields[4])
|
|
|
|
file_longitude = float(fields[5])
|
|
|
|
file_country_code = fields[8]
|
|
|
|
new_distance = math.sqrt(pow((latitude - file_latitude), 2) + pow((longitude - file_longitude), 2))
|
|
|
|
if distance > new_distance:
|
|
|
|
distance = new_distance
|
|
|
|
country = file_country_code
|
|
|
|
city = file_city_name
|
2021-08-26 13:27:43 -04:00
|
|
|
return Location(country, city)
|
2020-11-27 11:31:25 -05:00
|
|
|
|
2021-01-11 10:25:34 -05:00
|
|
|
@staticmethod
|
|
|
|
def distance_between_points(vertex1, vertex2):
|
2021-03-31 14:17:53 -04:00
|
|
|
"""
|
|
|
|
distance between points in an n-D Euclidean space
|
|
|
|
:param vertex1: point or vertex
|
|
|
|
:param vertex2: point or vertex
|
|
|
|
:return: float
|
|
|
|
"""
|
2021-01-11 10:25:34 -05:00
|
|
|
power = 0
|
2023-05-31 13:51:35 -04:00
|
|
|
for dimension, current_vertex in enumerate(vertex1):
|
|
|
|
power += math.pow(vertex2[dimension] - current_vertex, 2)
|
2021-01-11 10:25:34 -05:00
|
|
|
distance = math.sqrt(power)
|
|
|
|
return distance
|