modm_data.utils
1# Copyright 2022, Niklas Hauser 2# SPDX-License-Identifier: MPL-2.0 3 4from .math import Point, Line, VLine, HLine, Rectangle, Region 5from .helper import list_lstrip, list_rstrip, list_strip, pkg_file_exists, pkg_apply_patch, apply_patch 6from .anytree import ReversePreOrderIter 7from .path import root_path, ext_path, cache_path, patch_path 8from .xml import XmlReader 9 10__all__ = [ 11 "Point", 12 "Line", 13 "VLine", 14 "HLine", 15 "Rectangle", 16 "Region", 17 "list_lstrip", 18 "list_rstrip", 19 "list_strip", 20 "pkg_file_exists", 21 "pkg_apply_patch", 22 "apply_patch", 23 "ReversePreOrderIter", 24 "root_path", 25 "ext_path", 26 "cache_path", 27 "patch_path", 28 "XmlReader", 29]
class
Point:
13class Point: 14 __slots__ = ("type", "x", "y") 15 16 def __init__(self, *xy, type: Enum = None): 17 if isinstance(xy[0], tuple): 18 self.x = xy[0][0] 19 self.y = xy[0][1] 20 else: 21 self.x = xy[0] 22 self.y = xy[1] 23 self.type = type 24 25 def isclose(self, other, rtol: float = 1e-09, atol: float = 0.0) -> bool: 26 return math.isclose(self.x, other.x, rel_tol=rtol, abs_tol=atol) and math.isclose( 27 self.y, other.y, rel_tol=rtol, abs_tol=atol 28 ) 29 30 def distance_squared(self, other) -> float: 31 return math.pow(self.x - other.x, 2) + math.pow(self.y - other.y, 2) 32 33 def distance(self, other) -> float: 34 return math.sqrt(self.distance_squared(other)) 35 36 def __neg__(self): 37 return Point(-self.x, -self.y) 38 39 def __hash__(self): 40 return hash(f"{self.x} {self.y}") 41 42 def __repr__(self) -> str: 43 x = f"{self.x:.1f}" if isinstance(self.x, float) else self.x 44 y = f"{self.y:.1f}" if isinstance(self.y, float) else self.y 45 out = [x, y] if self.type is None else [x, y, self.type.name] 46 return f"({','.join(out)})"
class
Line:
49class Line: 50 class Direction(Enum): 51 ANGLE = 0 52 VERTICAL = 1 53 HORIZONTAL = 2 54 55 def __init__(self, *r, width: float = None, type: Enum = None): 56 if isinstance(r[0], Rectangle): 57 self.p0 = r[0].p0 58 self.p1 = r[0].p1 59 elif isinstance(r[0], Point): 60 self.p0 = r[0] 61 self.p1 = r[1] 62 elif isinstance(r[0], tuple): 63 self.p0 = Point(r[0][0], r[0][1]) 64 self.p1 = Point(r[1][0], r[1][1]) 65 else: 66 self.p0 = Point(r[0], r[1]) 67 self.p1 = Point(r[2], r[3]) 68 69 self.width = 0.1 if width is None else width 70 self.type = type 71 72 @cached_property 73 def bbox(self): 74 return Rectangle( 75 min(self.p0.x, self.p1.x), min(self.p0.y, self.p1.y), max(self.p0.x, self.p1.x), max(self.p0.y, self.p1.y) 76 ) 77 78 def isclose(self, other, rtol: float = 1e-09, atol: float = 0.0) -> bool: 79 return self.p0.isclose(other.p0, rtol, atol) and self.p1.isclose(other.p1, rtol, atol) 80 81 def contains(self, point, atol: float = 0.0) -> bool: 82 # if the point lies on the line (A-C---B), the distance A-C + C-B = A-B 83 ac = self.p0.distance_squared(point) 84 cb = self.p1.distance_squared(point) 85 ab = self.p0.distance_squared(self.p1) 86 return (ac + cb + math.pow(atol, 2)) <= ab 87 88 @property 89 def direction(self): 90 if math.isclose(self.p0.x, self.p1.x): 91 return Line.Direction.VERTICAL 92 if math.isclose(self.p0.y, self.p1.y): 93 return Line.Direction.HORIZONTAL 94 return Line.Direction.ANGLE 95 96 def specialize(self): 97 if self.direction == Line.Direction.VERTICAL: 98 return VLine(self.p0.x, self.p0.y, self.p1.y, self.width) 99 if self.direction == Line.Direction.HORIZONTAL: 100 return HLine(self.p0.y, self.p0.x, self.p1.x, self.width) 101 return Line(self.p0, self.p1, width=self.width) 102 103 def __repr__(self) -> str: 104 data = [repr(self.p0), repr(self.p1)] 105 if self.width: 106 data += [f"{self.width:.1f}"] 107 if self.type is not None: 108 data += [self.type.name] 109 return f"<{','.join(data)}>"
Line(*r, width: float = None, type: enum.Enum = None)
55 def __init__(self, *r, width: float = None, type: Enum = None): 56 if isinstance(r[0], Rectangle): 57 self.p0 = r[0].p0 58 self.p1 = r[0].p1 59 elif isinstance(r[0], Point): 60 self.p0 = r[0] 61 self.p1 = r[1] 62 elif isinstance(r[0], tuple): 63 self.p0 = Point(r[0][0], r[0][1]) 64 self.p1 = Point(r[1][0], r[1][1]) 65 else: 66 self.p0 = Point(r[0], r[1]) 67 self.p1 = Point(r[2], r[3]) 68 69 self.width = 0.1 if width is None else width 70 self.type = type
class
Line.Direction(enum.Enum):
Inherited Members
- enum.Enum
- name
- value
112class VLine(Line): 113 def __init__(self, x: float, y0: float, y1: float, width: float = None): 114 if y0 > y1: 115 y0, y1 = y1, y0 116 super().__init__(Point(x, y0), Point(x, y1), width=width) 117 self.length = y1 - y0 118 119 @property 120 def direction(self): 121 return Line.Direction.VERTICAL 122 123 def __repr__(self) -> str: 124 x = f"{self.p0.x:.1f}" if isinstance(self.p0.x, float) else self.p0.x 125 y0 = f"{self.p0.y:.1f}" if isinstance(self.p0.y, float) else self.p0.y 126 y1 = f"{self.p1.y:.1f}" if isinstance(self.p1.y, float) else self.p1.y 127 out = f"<X{x}:{y0},{y1}" 128 if self.width: 129 out += f"|{self.width:.1f}" 130 return out + ">"
133class HLine(Line): 134 def __init__(self, y: float, x0: float, x1: float, width: float = None): 135 if x0 > x1: 136 x0, x1 = x1, x0 137 super().__init__(Point(x0, y), Point(x1, y), width=width) 138 self.length = x1 - x0 139 140 @property 141 def direction(self): 142 return Line.Direction.HORIZONTAL 143 144 def __repr__(self) -> str: 145 y = f"{self.p0.y:.1f}" if isinstance(self.p0.y, float) else self.p0.y 146 x0 = f"{self.p0.x:.1f}" if isinstance(self.p0.x, float) else self.p0.x 147 x1 = f"{self.p1.x:.1f}" if isinstance(self.p1.x, float) else self.p1.x 148 out = f"<Y{y}:{x0},{x1}" 149 if self.width: 150 out += f"|{self.width:.1f}" 151 return out + ">"
class
Rectangle:
154class Rectangle: 155 __slots__ = ("_midpoint", "bottom", "height", "left", "p0", "p1", "right", "top", "width", "x", "y") 156 157 def __init__(self, *r): 158 # P0 is left, bottom 159 # P1 is right, top 160 if isinstance(r[0], pp.raw.FS_RECTF): 161 self.p0 = Point(r[0].left, r[0].bottom) 162 self.p1 = Point(r[0].right, r[0].top) 163 elif isinstance(r[0], Point): 164 self.p0 = r[0] 165 self.p1 = r[1] 166 elif isinstance(r[0], tuple): 167 self.p0 = Point(r[0][0], r[0][1]) 168 self.p1 = Point(r[1][0], r[1][1]) 169 else: 170 self.p0 = Point(r[0], r[1]) 171 self.p1 = Point(r[2], r[3]) 172 173 # Ensure the correct ordering of point values 174 if self.p0.x > self.p1.x: 175 self.p0.x, self.p1.x = self.p1.x, self.p0.x 176 if self.p0.y > self.p1.y: 177 self.p0.y, self.p1.y = self.p1.y, self.p0.y 178 179 # assert self.p0.x <= self.p1.x 180 # assert self.p0.y <= self.p1.y 181 182 self.x = self.p0.x 183 self.y = self.p0.y 184 self.left = self.p0.x 185 self.bottom = self.p0.y 186 187 self.right = self.p1.x 188 self.top = self.p1.y 189 190 self.width = self.p1.x - self.p0.x 191 self.height = self.p1.y - self.p0.y 192 self._midpoint = None 193 194 def contains(self, other) -> bool: 195 if isinstance(other, Point): 196 return self.bottom <= other.y <= self.top and self.left <= other.x <= self.right 197 # Comparing y-axis first may be faster for "content areas filtering" 198 # when doing subparsing of page content (like in tables) 199 return ( 200 self.bottom <= other.bottom 201 and other.top <= self.top 202 and self.left <= other.left 203 and other.right <= self.right 204 ) 205 206 def overlaps(self, other) -> bool: 207 return self.contains(other.p0) or self.contains(other.p1) 208 209 def isclose(self, other, rtol: float = 1e-09, atol: float = 0.0) -> bool: 210 return self.p0.isclose(other.p0, rtol, atol) and self.p1.isclose(other.p1, rtol, atol) 211 212 @property 213 def midpoint(self) -> Point: 214 if self._midpoint is None: 215 self._midpoint = Point((self.p1.x + self.p0.x) / 2, (self.p1.y + self.p0.y) / 2) 216 return self._midpoint 217 218 @property 219 def points(self) -> list[Point]: 220 return [self.p0, Point(self.right, self.bottom), self.p1, Point(self.left, self.top)] 221 222 def offset(self, offset): 223 return Rectangle(self.p0.x - offset, self.p0.y - offset, self.p1.x + offset, self.p1.y + offset) 224 225 def offset_x(self, offset): 226 return Rectangle(self.p0.x - offset, self.p0.y, self.p1.x + offset, self.p1.y) 227 228 def offset_y(self, offset): 229 return Rectangle(self.p0.x, self.p0.y - offset, self.p1.x, self.p1.y + offset) 230 231 def translated(self, point): 232 return Rectangle(self.p0.x + point.x, self.p0.y + point.y, self.p1.x + point.x, self.p1.y + point.y) 233 234 def rotated(self, rotation): 235 cos = math.cos(math.radians(rotation)) 236 sin = math.sin(math.radians(rotation)) 237 return Rectangle( 238 self.p0.x * cos - self.p0.y * sin, 239 self.p0.x * sin + self.p0.y * cos, 240 self.p1.x * cos - self.p1.y * sin, 241 self.p1.x * sin + self.p1.y * cos, 242 ) 243 244 def joined(self, other): 245 return Rectangle( 246 min(self.p0.x, other.p0.x), 247 min(self.p0.y, other.p0.y), 248 max(self.p1.x, other.p1.x), 249 max(self.p1.y, other.p1.y), 250 ) 251 252 def round(self, accuracy=0): 253 return Rectangle( 254 round(self.p0.x, accuracy), 255 round(self.p0.y, accuracy), 256 round(self.p1.x, accuracy), 257 round(self.p1.y, accuracy), 258 ) 259 260 def __hash__(self): 261 return hash(self.p0) + hash(self.p1) 262 263 def __repr__(self) -> str: 264 return f"[{repr(self.p0)},{repr(self.p1)}]"
Rectangle(*r)
157 def __init__(self, *r): 158 # P0 is left, bottom 159 # P1 is right, top 160 if isinstance(r[0], pp.raw.FS_RECTF): 161 self.p0 = Point(r[0].left, r[0].bottom) 162 self.p1 = Point(r[0].right, r[0].top) 163 elif isinstance(r[0], Point): 164 self.p0 = r[0] 165 self.p1 = r[1] 166 elif isinstance(r[0], tuple): 167 self.p0 = Point(r[0][0], r[0][1]) 168 self.p1 = Point(r[1][0], r[1][1]) 169 else: 170 self.p0 = Point(r[0], r[1]) 171 self.p1 = Point(r[2], r[3]) 172 173 # Ensure the correct ordering of point values 174 if self.p0.x > self.p1.x: 175 self.p0.x, self.p1.x = self.p1.x, self.p0.x 176 if self.p0.y > self.p1.y: 177 self.p0.y, self.p1.y = self.p1.y, self.p0.y 178 179 # assert self.p0.x <= self.p1.x 180 # assert self.p0.y <= self.p1.y 181 182 self.x = self.p0.x 183 self.y = self.p0.y 184 self.left = self.p0.x 185 self.bottom = self.p0.y 186 187 self.right = self.p1.x 188 self.top = self.p1.y 189 190 self.width = self.p1.x - self.p0.x 191 self.height = self.p1.y - self.p0.y 192 self._midpoint = None
def
contains(self, other) -> bool:
194 def contains(self, other) -> bool: 195 if isinstance(other, Point): 196 return self.bottom <= other.y <= self.top and self.left <= other.x <= self.right 197 # Comparing y-axis first may be faster for "content areas filtering" 198 # when doing subparsing of page content (like in tables) 199 return ( 200 self.bottom <= other.bottom 201 and other.top <= self.top 202 and self.left <= other.left 203 and other.right <= self.right 204 )
midpoint: Point
points: list[Point]
def
rotated(self, rotation):
234 def rotated(self, rotation): 235 cos = math.cos(math.radians(rotation)) 236 sin = math.sin(math.radians(rotation)) 237 return Rectangle( 238 self.p0.x * cos - self.p0.y * sin, 239 self.p0.x * sin + self.p0.y * cos, 240 self.p1.x * cos - self.p1.y * sin, 241 self.p1.x * sin + self.p1.y * cos, 242 )
class
Region:
267class Region: 268 def __init__(self, v0, v1, obj=None): 269 if v0 > v1: 270 v0, v1 = v1, v0 271 self.v0 = v0 272 self.v1 = v1 273 self.objs = [] if obj is None else [obj] 274 self.subregions = [] 275 276 def overlaps(self, o0, o1, atol=0) -> bool: 277 if o0 > o1: 278 o0, o1 = o1, o0 279 # if reg top is lower then o0 280 if (self.v1 + atol) <= o0: 281 return False 282 # if reg bottom is higher than o1 283 if o1 <= (self.v0 - atol): 284 return False 285 return True 286 287 def contains(self, v, atol=0) -> bool: 288 return self.v0 - atol <= v <= self.v1 + atol 289 290 @property 291 def delta(self) -> float: 292 return self.v1 - self.v0 293 294 def __repr__(self): 295 r = f"<{int(self.v0)}->{int(self.v1)}" 296 if self.objs: 297 r += f"|{len(self.objs)}" 298 if self.subregions: 299 r += f"|{repr(self.subregions)}" 300 return r + ">"
def
list_lstrip(input_values: list, strip_fn) -> list:
def
list_rstrip(input_values: list, strip_fn) -> list:
def
list_strip(input_values: list, strip_fn) -> list:
def
pkg_file_exists(pkg, file: pathlib.Path) -> bool:
def
pkg_apply_patch(pkg, patch: pathlib.Path, base_dir: pathlib.Path) -> bool:
def
apply_patch(patch_file: pathlib.Path, base_dir: pathlib.Path) -> bool:
44def apply_patch(patch_file: Path, base_dir: Path) -> bool: 45 cmds = [ 46 "patch", 47 "--strip=1", 48 "--ignore-whitespace", 49 "--reject-file=-", 50 "--forward", 51 "--posix", 52 "--directory", 53 Path(base_dir).absolute(), 54 "--input", 55 Path(patch_file).absolute(), 56 ] 57 58 def run(*args) -> bool: 59 result = subprocess.run(cmds + list(args), capture_output=True, text=True) 60 # Hunks that the conversion already fixed are ignored, which is fine 61 if result.returncode and re.search(r"failed|can't find|malformed|garbage", result.stdout + result.stderr): 62 print(result.stdout + result.stderr) 63 return False 64 return True 65 66 return run("--dry-run") and run()
class
ReversePreOrderIter(anytree.iterators.abstractiter.AbstractIter):
8class ReversePreOrderIter(AbstractIter): 9 @staticmethod 10 def _iter(children, filter_, stop, maxlevel): 11 for child_ in reversed(children): 12 if not AbstractIter._abort_at_level(2, maxlevel): 13 descendantmaxlevel = maxlevel - 1 if maxlevel else None 14 yield from ReversePreOrderIter._iter(child_.children, filter_, stop, descendantmaxlevel) 15 if stop(child_): 16 continue 17 if filter_(child_): 18 yield child_
Iterate over tree starting at node.
Base class for all iterators.
Keyword Args:
filter_: function called with every node as argument, node is returned if True.
stop: stop iteration at node if stop function returns True for node.
maxlevel (int): maximum descending in the node hierarchy.
Inherited Members
- anytree.iterators.abstractiter.AbstractIter
- AbstractIter
- node
- filter_
- stop
- maxlevel
def
root_path(path) -> pathlib.Path:
def
ext_path(path) -> pathlib.Path:
def
cache_path(path) -> pathlib.Path:
def
patch_path(path) -> pathlib.Path:
class
XmlReader:
17class XmlReader: 18 def __init__(self, path): 19 self.filename = path 20 self.tree = self._openDeviceXML(self.filename) 21 22 def __repr__(self): 23 return f"XMLReader({os.path.basename(self.filename)})" 24 25 def _openDeviceXML(self, filename): 26 LOGGER.debug("Opening XML file '%s'", os.path.basename(filename)) 27 xml_file = Path(filename).read_text("utf-8", errors="replace") 28 xml_file = re.sub(' xmlns="[^"]+"', "", xml_file, count=1).encode("utf-8") 29 xmltree = etree.fromstring(xml_file, parser=PARSER) 30 return xmltree 31 32 def queryTree(self, query): 33 """ 34 This tries to apply the query to the device tree and returns either 35 - an array of element nodes, 36 - an array of strings or 37 - None, if the query failed. 38 """ 39 response = None 40 try: 41 response = self.tree.xpath(query) 42 except Exception: 43 LOGGER.error(f"Query failed for '{query}'") 44 45 return response 46 47 def query(self, query, default=[]): 48 result = self.queryTree(query) 49 if result is not None: 50 sorted_results = [] 51 for r in result: 52 if r not in sorted_results: 53 sorted_results.append(r) 54 return sorted_results 55 56 return default 57 58 def compactQuery(self, query): 59 return self.query(query, None)
def
queryTree(self, query):
32 def queryTree(self, query): 33 """ 34 This tries to apply the query to the device tree and returns either 35 - an array of element nodes, 36 - an array of strings or 37 - None, if the query failed. 38 """ 39 response = None 40 try: 41 response = self.tree.xpath(query) 42 except Exception: 43 LOGGER.error(f"Query failed for '{query}'") 44 45 return response
This tries to apply the query to the device tree and returns either
- an array of element nodes,
- an array of strings or
- None, if the query failed.