From 347fbbfc9f052e82a6526a63d51b7744f3d47331 Mon Sep 17 00:00:00 2001 From: Ffhener Date: Fri, 14 Jul 2023 15:04:35 +0200 Subject: [PATCH] Line of Sight Visualizer: Init This is a partial rewrite of https://rop.nl/freifunk/line-of-sight.php. Webscraping got replaced by API-Calls. The main functionality is working, however more detailed information cant be shown as long as there are not available as attributes in the wiki. I consider this optional as you can still get all the relevant information in the wikiarticle, so its not strictly needed in Google Earth. Since big parts of the code are legacy I excluded them from linting. Otherwise a way bigger rewrite would be needed. The code still works as expected. --- .github/workflows/linter.yml | 2 +- www/line-of-sight.kml | 13 + www/line-of-sight.php | 456 +++++++++++++++++++++++++++++++++++ 3 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 www/line-of-sight.kml create mode 100644 www/line-of-sight.php diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index c5f8f35..16587bb 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -59,4 +59,4 @@ jobs: DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Do not lint 3rd party CSS and JavaScript libraries - FILTER_REGEX_EXCLUDE: .*/(css|javascript)/3rd-party/.* + FILTER_REGEX_EXCLUDE: .*/(css|javascript)/3rd-party/.*|.*line-of-sight\.php diff --git a/www/line-of-sight.kml b/www/line-of-sight.kml new file mode 100644 index 0000000..47de02d --- /dev/null +++ b/www/line-of-sight.kml @@ -0,0 +1,13 @@ + + Freifunk Berlin Line-of-Sight visualiser + 0 + 1 + 1 + 1 + + https://util.berlin.freifunk.net/line-of-sight.php + onChange + onRequest + VARS=[cameraLon],[cameraLat],[cameraAlt] + + diff --git a/www/line-of-sight.php b/www/line-of-sight.php new file mode 100644 index 0000000..537c686 --- /dev/null +++ b/www/line-of-sight.php @@ -0,0 +1,456 @@ +"; +// print_r ($standorte); +// exit(); + + foreach ($standorte as $standort => $item) { + unset($location); + $location['url'] = "https://wiki.freifunk.net/" . $standort; + $location['name'] = str_replace('Berlin:Standorte:', '', $standort); + + $location['lat'] = $item['printouts']['Hat Koordinaten'][0]['lat']; + $location['lon'] = $item['printouts']['Hat Koordinaten'][0]['lon']; + + if ($item['printouts']['Hat ueber NN']) { + $location['alt'] = $item['printouts']['Hat ueber NN']['0']; + } else { + $location['alt'] = 0; + } + + $locations[] = $location; + } + + // And write the array to the cache file + file_put_contents($cachefile, serialize($locations)); +} + +// Parse the variables Google Earth passes with each refresh as instructed by the first KML file. +list ($cameraLon, $cameraLat, $cameraAlt) = explode(",", $_GET['VARS']); + +// Create links if the eye altitude is below 250 meters +if (isset($cameraAlt) && $cameraAlt < 250) { + $survey_location['lat'] = $cameraLat; + $survey_location['lon'] = $cameraLon; + $survey_location['alt'] = $cameraAlt - 6; + $survey_location['name'] = "Survey Location"; + + foreach ($locations as $location) { + if ($location['alt'] > 0) { + unset($link); + $link['name'] = $location['name']; + $link['lat'] = $location['lat']; + $link['lon'] = $location['lon']; + $link['alt'] = $location['alt']; + $link['distance'] = distance($survey_location, $link) . " m"; + $link['azimuth to'] = bearing($survey_location, $link) . "°"; + $link['elevation to'] = elevation($survey_location, $link) . "°"; + $link['azimuth from'] = bearing($link, $survey_location) . "°"; + $link['fspl_2.4'] = fspl($link['distance'], 2400000000); + $link['fspl_5'] = fspl($link['distance'], 5000000000); + $links[] = $link; + } + } + + // Sort the $links array by direction to $survey_location, starting north. + if (isset($links)) { + foreach ($links as $key => $link) { + $direction[$key] = $link['azimuth to']; + } + array_multisort($direction, SORT_ASC, SORT_NUMERIC, $links); + } + + $locations[] = $survey_location; +} + +// This creates $kml which is what's output in the end +$kml = headerKML(); + +// First all the nodes in a non-expanding folder with a placemark icon. +$kml .= ''; +$kml .= 'Locations'; +$kml .= ''; +foreach ($locations as $location) { + if ($location['lat'] > 0) { + $kml .= ''; + $kml .= '' . $location['name'] . ''; + $kml .= '' . $location['name'] . ''; + $kml .= ''; + foreach ($location as $name => $val) { + if ($name !== 'name' && $name !== 'url') { + $kml .= ''; + } + } + $kml .= '
' . $name . ': ' . $val . '
]]>
'; + + $kml .= '#msn_placemark_circle'; + $kml .= ''; + $kml .= 'absolute'; + $kml .= '' . $location['lon'] . ',' . $location['lat'] . ',' . $location['alt'] . ''; + $kml .= ''; + $kml .= '
' . "\n\n"; + } +} +$kml .= '
'; + + +// And then all the links if we have a survey location (i.e. we're under 250 m altitude) +if (isset($links)) { + $kml .= "Links (clockwise from north)11\n\n"; + foreach ($links as $link) { + $kml .= ''; + $kml .= 'Link to ' . $link['name'] . ' (' . number_format($link['distance'] / 1000, 1) . ' km)1'; + $kml .= ''; + + $kml .= ''; + $kml .= 'Line with screen to ground'; + $kml .= '#line'; + $kml .= ''; + $kml .= '1'; + $kml .= 'absolute'; + $kml .= '' . $link['lon'] . ',' . $link['lat'] . ',' . $link['alt'] . ',' . $survey_location['lon'] . ',' . $survey_location['lat'] . ',' . ( $survey_location['alt'] ) . ''; + $kml .= ''; + $kml .= '' . "\n"; + + $kml .= ''; + $kml .= '2.4 GHz fresnel zone'; + $kml .= ''; + $kml .= 'Link to ' . $link['name'] . ' (' . number_format($link['distance'] / 1000, 1) . ' km)'; + $kml .= ''; + $kml .= ''; + $kml .= ''; + $kml .= ''; + $kml .= ''; + $kml .= ''; + $kml .= '
distance: ' . $link['distance'] . '
azimuth to: ' . $link['azimuth to'] . '
elevation to: ' . $link['elevation to'] . '
azimuth from: ' . $link['azimuth from'] . '
FSPL: ' . $link['fspl_2.4'] . ' dB @ 2.4 GHz
' . $link['fspl_5'] . ' dB @ 5 GHz
]]>
'; + $kml .= '#polygon-transparent'; + $kml .= '' . "\n\n"; + $kml .= makeFresnelPolygons($survey_location, $link, 2400000000, 20); + $kml .= '' . "\n\n"; + $kml .= '
'; + + $kml .= ''; + $kml .= '5 GHz fresnel zone'; + $kml .= '#polygon'; + $kml .= '' . "\n\n"; + $kml .= makeFresnelPolygons($survey_location, $link, 5000000000, 20); + $kml .= '' . "\n\n"; + $kml .= ''; + + $kml .= '
' . "\n\n"; + } + $kml .= '
'; +} + +$kml .= '' . "\n"; + +echo $kml; + + +// The arguments to distance, bearing and elevation functions are arrays that expected to have keys +// named 'lat', 'lon' and (except for bearing) 'alt'. + +function distance($from, $to) +{ + + $lat1 = deg2rad($from['lat']); + $lon1 = deg2rad($from['lon']); + $lat2 = deg2rad($to['lat']); + $lon2 = deg2rad($to['lon']); + + $theta = $lon1 - $lon2; + $dist = rad2deg(acos(sin($lat1) * sin($lat2) + cos($lat1) * cos($lat2) * cos($theta))) * ( CIRCUMFERENCE_OF_EARTH / 360 ); + + // Add the diagonal component using pythagoras + // (even if diff minimal in most of our cases) + $alt_diff = abs($from['alt'] - $to['alt']); + $dist = sqrt(($alt_diff * $alt_diff) + ($dist * $dist)); + + return intval($dist); +} + +function bearing($from, $to) +{ + + $lat1 = deg2rad($from['lat']); + $lon1 = deg2rad($from['lon']); + $lat2 = deg2rad($to['lat']); + $lon2 = deg2rad($to['lon']); + + //difference in longitudinal coordinates + $dLon = $lon2 - $lon1; + + //difference in the phi of latitudinal coordinates + $dPhi = log(tan($lat2 / 2 + M_PI / 4) / tan($lat1 / 2 + M_PI / 4)); + + //we need to recalculate $dLon if it is greater than pi + if (abs($dLon) > M_PI) { + if ($dLon > 0) { + $dLon = (2 * M_PI - $dLon) * -1; + } else { + $dLon = 2 * M_PI + $dLon; + } + } + + //return the angle, normalized + return ( rad2deg(atan2($dLon, $dPhi)) + 360 ) % 360; +} + +function elevation($from, $to) +{ + return intval(rad2deg(atan2($to['alt'] - $from['alt'], distance($from, $to)))); +} + +function fspl($dist, $freq) +{ + return intval(20 * log10(((4 * M_PI) / SPEED_OF_LIGHT) * $dist * $freq)); +} + +function makeFresnelPolygons($from, $to, $freq, $steps_in_circles) +{ + // How many degrees is a meter? + $lat_meter = 1 / ( CIRCUMFERENCE_OF_EARTH / 360 ); + $lon_meter = (1 / cos(deg2rad($from['lat']))) * $lat_meter; + + + $distance = distance($from, $to); + $bearing = bearing($from, $to); + $wavelen = SPEED_OF_LIGHT / $freq; // Speed of light + + + // $steps_in_path is an array of values between 0 (at $from) and 1 (at $to) + // These are the distances where new polygons are started to show elipse + + // First we do that at some fixed fractions of path + $steps_in_path = array(0,0.25,0.4); + + // Then we add some steps set in meters because that looks better at + // the ends of the beam + foreach (array(0.3,1,2,4,7,10,20,40,70,100) as $meters) { + // calculate fraction of path + $steps_in_path[] = $meters / $distance; + } + + // Add the reverse of these steps on other side of beam + $temp = $steps_in_path; + foreach ($temp as $step) { + $steps_in_path[] = 1 - $step; + } + + // Sort and remove duplicates + sort($steps_in_path, SORT_NUMERIC); + $steps_in_path = array_unique($steps_in_path); + + // Fill array $rings with arrays that each hold a ring of points surrounding the beam + foreach ($steps_in_path as $step) { + $centerpoint['lat'] = $from['lat'] + ( ($to['lat'] - $from['lat']) * $step ); + $centerpoint['lon'] = $from['lon'] + ( ($to['lon'] - $from['lon']) * $step ); + $centerpoint['alt'] = $from['alt'] + ( ($to['alt'] - $from['alt']) * $step ); + + // Fresnel radius calculation + $d1 = $distance * $step; + $d2 = $distance - $d1; + $radius = sqrt(($wavelen * $d1 * $d2) / $distance); + + // Bearing of line perpendicular to bearing of line of sight. + $ring_bearing = $bearing + 90 % 360; + + unset($ring); + for ($n = 0; $n < $steps_in_circles; $n++) { + $angle = $n * ( 360 / $steps_in_circles ); + $vertical_factor = cos(deg2rad($angle)); + $horizontal_factor = sin(deg2rad($angle)); + $lat_factor = cos(deg2rad($ring_bearing)) * $horizontal_factor; + $lon_factor = sin(deg2rad($ring_bearing)) * $horizontal_factor; + + $new_point['lat'] = $centerpoint['lat'] + ($lat_factor * $lat_meter * $radius); + $new_point['lon'] = $centerpoint['lon'] + ($lon_factor * $lon_meter * $radius); + $new_point['alt'] = $centerpoint['alt'] + ($vertical_factor * $radius); + + $ring[] = $new_point; + } + $rings[] = $ring; + } + + // Make the polygons + + // since polygons connect this ring with next, skip last one. + for ($ring_nr = 0; $ring_nr < count($rings) - 1; $ring_nr++) { + $next_ring_nr = $ring_nr + 1; + + for ($point_nr = 0; $point_nr < $steps_in_circles; $point_nr++) { + $next_point_nr = $point_nr + 1; + if ($point_nr == $steps_in_circles - 1) { + $next_point_nr = 0; + } + + unset($polygon); + $polygon[] = $rings[$ring_nr][$point_nr]; + $polygon[] = $rings[$next_ring_nr][$point_nr]; + $polygon[] = $rings[$next_ring_nr][$next_point_nr]; + $polygon[] = $rings[$ring_nr][$next_point_nr]; + + $polygons[] = $polygon; + } + } + + $ret = ''; + + foreach ($polygons as $polygon) { + $ret .= 'absolute'; + + foreach ($polygon as $point) { + $ret .= $point['lon'] . ',' . $point['lat'] . ',' . $point['alt'] . " "; + } + + $ret .= ''; + } + + return $ret; +} + + + +function headerKML() +{ + + $kml = << + + + line-of-sight.php + 1 + + + + + normal + #sn_placemark_circle + + + highlight + #sh_placemark_circle_highlight + + + + + +HEREDOC; + + return $kml; +} + +function balloonCSS() +{ + + // There is no global stylesheet in Google Earth, so this needs to be appended to each balloon to make it display nicely. + $css = << + a:link {text-decoration: none;} + td.left {text-align: right; vertical-align: top; margin-bottom: 5px;} + td, h2 {white-space: nowrap; font-family: verdana;} + +HEREDOC; + + return $css; +}