'North', 'NE' => 'North-East', 'E' => 'East', 'SE' => 'South-East', 'S' => 'South', 'SW' => 'South-West', 'W' => 'West', 'NW' => 'North-West', 'C' => 'Centre']; /** A room this close (degrees) to the line between two zones is marked "on the boundary". */ public const BORDER_DEG = 5.0; public static function wrap(float $deg): float { $d = fmod($deg, 360.0); return $d < 0 ? $d + 360.0 : $d; } /** 8-direction zone (45° each, centred on N, NE, …) for a true bearing. */ public static function zone(float $bearing): string { return self::DIRS[(int)floor(self::wrap($bearing + 22.5) / 45.0) % 8]; } public const DIRS16 = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']; /** 16-zone name (22.5° each, centred on N, NNE, …) for a true bearing. */ public static function zone16(float $bearing): string { return self::DIRS16[(int)floor(self::wrap($bearing + 11.25) / 22.5) % 16]; } /** Degrees from the bearing to the nearest zone boundary. */ public static function distanceToBoundary(float $bearing): float { $inSector = fmod(self::wrap($bearing + 22.5), 45.0); // 0..45 return min($inSector, 45.0 - $inSector); } /** The neighbouring zone the bearing is closest to (for boundary notes). */ public static function neighbourZone(float $bearing): string { $inSector = fmod(self::wrap($bearing + 22.5), 45.0); return self::zone($bearing + ($inSector < 22.5 ? -22.5 : 22.5)); } /** Southern Hemisphere "mirrored" reading: north and south swapped, east and west kept. */ public static function mirror(string $dir): string { return ['N' => 'S', 'NE' => 'SE', 'E' => 'E', 'SE' => 'NE', 'S' => 'N', 'SW' => 'NW', 'W' => 'W', 'NW' => 'SW', 'C' => 'C'][$dir] ?? $dir; } /** Image bearing (clockwise from up) of point B seen from point A, correcting for the image's aspect ratio. */ public static function imageBearing(array $from, array $to, float $aspect): float { $dx = ($to[0] - $from[0]) * $aspect; // aspect = width / height $dy = ($to[1] - $from[1]); return self::wrap(rad2deg(atan2($dx, -$dy))); } /** Area-weighted centroid of a polygon given as [[x,y],…]; falls back to the mean of points. */ public static function centroid(array $pts, float $aspect = 1.0): array { $n = count($pts); if ($n === 0) return [50.0, 50.0]; if ($n < 3) { return [array_sum(array_column($pts, 0)) / $n, array_sum(array_column($pts, 1)) / $n]; } $a = $cx = $cy = 0.0; for ($i = 0; $i < $n; $i++) { [$x1, $y1] = $pts[$i]; [$x2, $y2] = $pts[($i + 1) % $n]; $x1 *= $aspect; $x2 *= $aspect; $cr = $x1 * $y2 - $x2 * $y1; $a += $cr; $cx += ($x1 + $x2) * $cr; $cy += ($y1 + $y2) * $cr; } if (abs($a) < 1e-9) { return [array_sum(array_column($pts, 0)) / $n, array_sum(array_column($pts, 1)) / $n]; } return [$cx / (3 * $a) / $aspect, $cy / (3 * $a)]; } /** Axis-aligned bounding box [minX, minY, maxX, maxY] of points. */ public static function bbox(array $pts): array { $xs = array_column($pts, 0); $ys = array_column($pts, 1); return [min($xs), min($ys), max($xs), max($ys)]; } /** * Rotation of the house walls away from the true compass (0 = walls run N–S/E–W). * Taken from the longest outline edge. Returns degrees in -45..45. */ public static function houseRotation(array $outline, float $aspect, float $trueNorthImageBearing): ?float { $n = count($outline); if ($n < 3) return null; $best = 0.0; $bestLen = -1.0; for ($i = 0; $i < $n; $i++) { $a = $outline[$i]; $b = $outline[($i + 1) % $n]; $len = hypot(($b[0] - $a[0]) * $aspect, $b[1] - $a[1]); if ($len > $bestLen) { $bestLen = $len; $best = self::imageBearing($a, $b, $aspect); } } $trueEdge = self::wrap($best - $trueNorthImageBearing); $r = fmod($trueEdge, 90.0); return $r > 45.0 ? $r - 90.0 : $r; } /** * Is a point inside the central ninth of the house (the Brahmasthan), * measured along the house's own walls? */ public static function inCentre(array $pt, array $centre, array $outline, float $aspect, float $wallRotationImage): bool { // rotate everything so the house walls are axis-aligned, then use the middle third of each side $rot = deg2rad(-$wallRotationImage); $tf = function (array $p) use ($rot, $aspect, $centre) { $x = ($p[0] - $centre[0]) * $aspect; $y = $p[1] - $centre[1]; return [$x * cos($rot) - $y * sin($rot), $x * sin($rot) + $y * cos($rot)]; }; $o = array_map($tf, $outline); [$minX, $minY, $maxX, $maxY] = self::bbox($o); [$px, $py] = $tf($pt); $w3 = ($maxX - $minX) / 3; $h3 = ($maxY - $minY) / 3; return $px > $minX + $w3 && $px < $maxX - $w3 && $py > $minY + $h3 && $py < $maxY - $h3; } }