Empty TerminationPoint list
[oam.git] / code / network-generator / network_generation / model / python / hexagon.py
1 # Copyright 2023 highstreet technologies GmbH
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 # inspired by http://www.redblobgames.com/grids/hexagons/
16
17 # !/usr/bin/python
18
19 from __future__ import division, print_function
20
21 import collections
22 import math
23 from typing import NamedTuple
24
25 from network_generation.model.python.geo_location import GeoLocation
26 from network_generation.model.python.point import Point
27
28
29 class Hex:
30     def __init__(self, q: float, r: float, s: float) -> None:
31         if round(q + r + s) != 0:
32             raise ValueError("The sum of q, r, and s must be 0.")
33         self.q = q
34         self.r = r
35         self.s = s
36
37     def __str__(self) -> str:
38         return f"q: {self.q}, r: {self.r}, s: {self.s}"
39
40
41 def hex_add(a: Hex, b: Hex) -> Hex:
42     return Hex(a.q + b.q, a.r + b.r, a.s + b.s)
43
44
45 def hex_subtract(a: Hex, b: Hex) -> Hex:
46     return Hex(a.q - b.q, a.r - b.r, a.s - b.s)
47
48
49 def hex_scale(a: Hex, k: int) -> Hex:
50     return Hex(a.q * k, a.r * k, a.s * k)
51
52
53 def hex_rotate_left(a: Hex) -> Hex:
54     return Hex(-a.s, -a.q, -a.r)
55
56
57 def hex_rotate_right(a: Hex) -> Hex:
58     return Hex(-a.r, -a.s, -a.q)
59
60
61 hex_directions: list[Hex] = [
62     Hex(1, 0, -1),
63     Hex(1, -1, 0),
64     Hex(0, -1, 1),
65     Hex(-1, 0, 1),
66     Hex(-1, 1, 0),
67     Hex(0, 1, -1),
68 ]
69
70
71 def hex_direction(direction: int) -> Hex:
72     return hex_directions[direction]
73
74
75 def hex_neighbor(hex: Hex, direction: int) -> Hex:
76     return hex_add(hex, hex_direction(direction))
77
78
79 hex_diagonals: list[Hex] = [
80     Hex(2, -1, -1),
81     Hex(1, -2, 1),
82     Hex(-1, -1, 2),
83     Hex(-2, 1, 1),
84     Hex(-1, 2, -1),
85     Hex(1, 1, -2),
86 ]
87
88
89 def hex_diagonal_neighbor(hex: Hex, direction: int) -> Hex:
90     return hex_add(hex, hex_diagonals[direction])
91
92
93 def hex_length(hex: Hex) -> float:
94     return (abs(hex.q) + abs(hex.r) + abs(hex.s)) // 2
95
96
97 def hex_distance(a: Hex, b: Hex) -> float:
98     return hex_length(hex_subtract(a, b))
99
100
101 def hex_round(hex: Hex) -> Hex:
102     qi = round(hex.q)
103     ri = round(hex.r)
104     si = round(hex.s)
105     q_diff = abs(qi - hex.q)
106     r_diff = abs(ri - hex.r)
107     s_diff = abs(si - hex.s)
108     if q_diff > r_diff and q_diff > s_diff:
109         qi = -ri - si
110     else:
111         if r_diff > s_diff:
112             ri = -qi - si
113         else:
114             si = -qi - ri
115     return Hex(qi, ri, si)
116
117
118 def hex_lerp(a: Hex, b: Hex, t: float) -> Hex:  # linearly interpolation
119     return Hex(
120         a.q * (1 - t) + b.q * t,
121         a.r * (1 - t) + b.r * t,
122         a.s * (1 - t) + b.s * t,
123     )
124
125
126 def hex_linedraw(a: Hex, b: Hex) -> list[Hex]:
127     N: float = hex_distance(a, b)
128     a_nudge: Hex = Hex(a.q + 1e-06, a.r + 1e-06, a.s - 2e-06)
129     b_nudge: Hex = Hex(b.q + 1e-06, b.r + 1e-06, b.s - 2e-06)
130     results: list[Hex] = []
131     step: float = 1 / max(N, 1)
132     for i in range(0, int(N) + 1):
133         results.append(hex_round(hex_lerp(a_nudge, b_nudge, step * i)))
134     return results
135
136
137 OffsetCoord = collections.namedtuple("OffsetCoord", ["col", "row"])
138
139 EVEN: int = 1
140 ODD: int = -1
141
142
143 def qoffset_from_cube(offset: float, hex: Hex) -> OffsetCoord:
144     col = hex.q
145     row = hex.r + (hex.q + offset * (int(hex.q) & 1)) // 2
146     if offset != EVEN and offset != ODD:
147         raise ValueError("offset must be EVEN (+1) or ODD (-1)")
148     return OffsetCoord(col, row)
149
150
151 def qoffset_to_cube(offset: int, offsetCoord: OffsetCoord) -> Hex:
152     q = offsetCoord.col
153     r = (
154         offsetCoord.row
155         - (offsetCoord.col + offset * (offsetCoord.col & 1)) // 2
156     )
157     s = -q - r
158     if offset != EVEN and offset != ODD:
159         raise ValueError("offset must be EVEN (+1) or ODD (-1)")
160     return Hex(q, r, s)
161
162
163 def roffset_from_cube(offset: float, hex: Hex) -> OffsetCoord:
164     col = hex.q + (hex.r + offset * (int(hex.r) & 1)) // 2
165     row = hex.r
166     if offset != EVEN and offset != ODD:
167         raise ValueError("offset must be EVEN (+1) or ODD (-1)")
168     return OffsetCoord(col, row)
169
170
171 def roffset_to_cube(offset: float, hex: OffsetCoord) -> Hex:
172     q = hex.col - (hex.row + offset * (int(hex.row) & 1)) // 2
173     r = hex.row
174     s = -q - r
175     if offset != EVEN and offset != ODD:
176         raise ValueError("offset must be EVEN (+1) or ODD (-1)")
177     return Hex(q, r, s)
178
179
180 DoubledCoord = collections.namedtuple("DoubledCoord", ["col", "row"])
181
182
183 def qdoubled_from_cube(hex: Hex) -> DoubledCoord:
184     col = hex.q
185     row = 2 * hex.r + hex.q
186     return DoubledCoord(col, row)
187
188
189 def qdoubled_to_cube(doubledCoord: DoubledCoord) -> Hex:
190     q = doubledCoord.col
191     r = (doubledCoord.row - doubledCoord.col) // 2
192     s = -q - r
193     return Hex(q, r, s)
194
195
196 def rdoubled_from_cube(hex: Hex) -> DoubledCoord:
197     col = 2 * hex.q + hex.r
198     row = hex.r
199     return DoubledCoord(col, row)
200
201
202 def rdoubled_to_cube(doubledCoord: DoubledCoord) -> Hex:
203     q = (doubledCoord.col - doubledCoord.row) // 2
204     r = doubledCoord.row
205     s = -q - r
206     return Hex(q, r, s)
207
208
209 Orientation = collections.namedtuple(
210     "Orientation",
211     ["f0", "f1", "f2", "f3", "b0", "b1", "b2", "b3", "start_angle"],
212 )
213
214
215 # Layout = collections.namedtuple("Layout", ["orientation", "size", "origin"])
216 class Layout(NamedTuple):
217     orientation: Orientation
218     size: Point
219     origin: Point
220
221
222 layout_pointy: Orientation = Orientation(
223     math.sqrt(3.0),
224     math.sqrt(3.0) / 2.0,
225     0.0,
226     3.0 / 2.0,
227     math.sqrt(3.0) / 3.0,
228     -1.0 / 3.0,
229     0.0,
230     2.0 / 3.0,
231     0.5,
232 )
233 layout_flat: Orientation = Orientation(
234     3.0 / 2.0,
235     0.0,
236     math.sqrt(3.0) / 2.0,
237     math.sqrt(3.0),
238     2.0 / 3.0,
239     0.0,
240     -1.0 / 3.0,
241     math.sqrt(3.0) / 3.0,
242     0.0,
243 )
244
245
246 def hex_to_pixel(layout: Layout, hex: Hex) -> Point:
247     M = layout.orientation
248     size = layout.size
249     origin = layout.origin
250     x = (M.f0 * hex.q + M.f1 * hex.r) * size.x
251     y = (M.f2 * hex.q + M.f3 * hex.r) * size.y
252     return Point(x + origin.x, y + origin.y)
253
254
255 def pixel_to_hex(layout: Layout, p: Point) -> Hex:
256     M = layout.orientation
257     size = layout.size
258     origin = layout.origin
259     pt = Point((p.x - origin.x) / size.x, (p.y - origin.y) / size.y)
260     q = M.b0 * pt.x + M.b1 * pt.y
261     r = M.b2 * pt.x + M.b3 * pt.y
262     return Hex(q, r, -q - r)
263
264
265 def hex_corner_offset(layout: Layout, corner: int) -> Point:
266     M = layout.orientation
267     size = layout.size
268     angle = 2.0 * math.pi * (M.start_angle - corner) / 6.0
269     return Point(size.x * math.cos(angle), size.y * math.sin(angle))
270
271
272 def polygon_corners(layout: Layout, hex: Hex) -> list[Point]:
273     corners: list[Point] = []
274     center = hex_to_pixel(layout, hex)
275     for i in range(0, 6):
276         offset = hex_corner_offset(layout, i)
277         corners.append(Point(center.x + offset.x, center.y + offset.y))
278     return corners
279
280
281 def hex_to_geo_location(
282     layout: Layout, hex: Hex, reference: GeoLocation
283 ) -> GeoLocation:
284     hexPoint: Point = hex_to_pixel(layout, hex)
285     return reference.point_to_geo_location(hexPoint)