Fresnelzonen/line-of-sight.php
2016-12-28 14:48:52 +01:00

475 lines
15 KiB
PHP
Executable file

<?php
define ('SPEED_OF_LIGHT', 299792458); // in m/s
define ('CIRCUMFERENCE_OF_EARTH', 40075000); // in m
$cachefile = 'cached_locations';
// If the cache file exists and it is younger than 1 min and 'flush' get var is is not set ...
if (file_exists($cachefile)) {
if (time() - filectime($cachefile) < 60 && !isset($_GET['flush'])) {
// ... then read cache file and populate the $locations array with it
$locations = unserialize(file_get_contents($cachefile));
}
}
if ( !isset($locations) ) {
// Otherwise scrape the information from the freifunk wiki, getting all info about the berlin nodes
$index = file_get_contents('https://wiki.freifunk.net/Kategorie:Hamburg/Richtfunkstandort');
preg_match_all("|<a href=\"/Hamburg/Richtfunknetz:(.*?)\" title=\"Hamburg/Richtfunknetz:(.*?)\"|", $index, $standorte, PREG_SET_ORDER);
// echo "<pre>";
// print_r ($standorte);
// exit();
foreach ($standorte as $standort) {
unset($location);
$location['url'] = "https://wiki.freifunk.net/Hamburg/Richtfunknetz:" . $standort[1];
$location['name'] = $standort[2];
$standort_page_src = file_get_contents($location['url'] . '?action=raw');
preg_match("/{{Infobox Freifunk-Standort(.*?)\n}}/s", $standort_page_src, $matches);
$infobox = $matches[1];
foreach( explode( "| ", $infobox) as $pair) {
list($name, $value) = explode(" = ", $pair);
if ( strlen(trim($name)) && strlen(trim($value)) ) {
$value = trim($value);
$value = preg_replace('/\[\[(.*?)\|(.*?)\]\]/', '<a href="https://wiki.freifunk.net/$1">$2</a>', $value);
$value = preg_replace('/\[\[(.*?)\]\]/', '<a href="https://wiki.freifunk.net/$1">$1</a>', $value);
$value = preg_replace('/\{\{Knoten (.*?)\}\},?\s?/', '$1 (<a href="http://monitor.berlin.freifunk.net/host.php?h=$1">Monitor</a>, <a href="http://openwifimap.net//index.html#detail?node=$1.olsr">OWM</a>)<br>', $value);
$value = preg_replace('/\[(.*?) (.*?)\]/', '<a href="$1">$2</a>', $value);
$value = preg_replace('/([A-Za-z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/', '<a href="mailto:$1">$1</a>', $value);
$location[$name]=$value;
}
}
if (trim($location['Koordinaten'])) {
list($location['lat'], $location['lon']) = explode(",", $location['Koordinaten']);
unset($location['Koordinaten']);
}
if (strlen(trim($location['ueberNN'])) == 0) {
$location['alt'] = 0;
} else {
$location['alt'] = $location['ueberNN'];
}
unset($location['ueberNN']);
$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) . "&deg;";
$link['elevation to'] = elevation($survey_location, $link) . "&deg;";
$link['azimuth from'] = bearing($link, $survey_location) . "&deg;";
$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 .= '<Folder>';
$kml .= '<name>Locations</name>';
$kml .= '<Style><ListStyle><listItemType>checkHideChildren</listItemType><ItemIcon><href>http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png</href></ItemIcon></ListStyle></Style>';
foreach ($locations as $location) {
if ($location['lat'] > 0){
$kml .= '<Placemark>';
$kml .= '<name>' . $location['name'] . '</name>';
$kml .= '<description><![CDATA[' . balloonCSS();
$kml .= '<h2><a href="' . $location['url'] . '">' . $location['name'] . '</a></h2>';
$kml .= '<table>';
foreach ($location as $name => $val) {
if ($name !== 'name' && $name !== 'url') {
$kml .= '<tr><td class="left">' . $name . ': </td><td>' . $val . '</td></tr>';
}
}
$kml .= '</table>]]></description>';
$kml .= '<styleUrl>#msn_placemark_circle</styleUrl>';
$kml .= '<Point>';
$kml .= '<altitudeMode>absolute</altitudeMode>';
$kml .= '<coordinates>' . $location['lon'] . ',' . $location['lat'] . ',' . $location['alt'] . '</coordinates>';
$kml .= '</Point>';
$kml .= '</Placemark>' . "\n\n";
}
}
$kml .= '</Folder>';
// And then all the links if we have a survey location (i.e. we're under 250 m altitude)
if (isset($links)) {
$kml .= "<Folder><name>Links (clockwise from north)</name><open>1</open><visibility>1</visibility>\n\n";
foreach ($links as $link) {
$kml .= '<Folder>';
$kml .= '<name>Link to ' . $link['name'] . ' (' . number_format($link['distance'] / 1000, 1) . ' km)</name><visibility>1</visibility>';
$kml .= '<Style><ListStyle><listItemType>checkHideChildren</listItemType><ItemIcon><href>empty_icon.png</href></ItemIcon></ListStyle></Style>';
$kml .= '<Placemark>';
$kml .= '<name>Line with screen to ground</name>';
$kml .= '<styleUrl>#line</styleUrl>';
$kml .= '<LineString>';
$kml .= '<extrude>1</extrude>';
$kml .= '<altitudeMode>absolute</altitudeMode>';
$kml .= '<coordinates>' . $link['lon'] . ',' . $link['lat'] . ',' . $link['alt'] . ',' . $survey_location['lon'] . ',' . $survey_location['lat'] . ',' . ( $survey_location['alt'] ) . '</coordinates>';
$kml .= '</LineString>';
$kml .= '</Placemark>' . "\n";
$kml .= '<Placemark>';
$kml .= '<name>2.4 GHz fresnel zone</name>';
$kml .= '<snippet></snippet>';
$kml .= '<description><![CDATA[' . balloonCSS();
$kml .= '<h2>Link to ' . $link['name'] . ' (' . number_format($link['distance'] / 1000, 1) . ' km)</h2>';
$kml .= '<table>';
$kml .= '<tr><td class="left">distance: </td><td>' . $link['distance'] . '</td></tr>';
$kml .= '<tr><td class="left">azimuth to: </td><td>' . $link['azimuth to'] . '</td></tr>';
$kml .= '<tr><td class="left">elevation to: </td><td>' . $link['elevation to'] . '</td></tr>';
$kml .= '<tr><td class="left">azimuth from: </td><td>' . $link['azimuth from'] . '</td></tr>';
$kml .= '<tr><td class="left"><a href="https://en.wikipedia.org/wiki/Free-space_path_loss">FSPL</a>: </td><td>' . $link['fspl_2.4'] . ' dB @ 2.4 GHz<br>' . $link['fspl_5'] . ' dB @ 5 GHz</td></tr>';
$kml .= '</table>]]></description>';
$kml .= '<styleUrl>#polygon-transparent</styleUrl>';
$kml .= '<MultiGeometry>' . "\n\n";
$kml .= makeFresnelPolygons( $survey_location, $link, 2400000000, 20);
$kml .= '</MultiGeometry>' . "\n\n";
$kml .= '</Placemark>';
$kml .= '<Placemark>';
$kml .= '<name>5 GHz fresnel zone</name>';
$kml .= '<styleUrl>#polygon</styleUrl>';
$kml .= '<MultiGeometry>' . "\n\n";
$kml .= makeFresnelPolygons( $survey_location, $link, 5000000000,20);
$kml .= '</MultiGeometry>' . "\n\n";
$kml .= '</Placemark>';
$kml .= '</Folder>' . "\n\n";
}
$kml .= '</Folder>';
}
$kml .= '</Document></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 .= '<Polygon><altitudeMode>absolute</altitudeMode><outerBoundaryIs><LinearRing><coordinates>';
foreach ($polygon as $point) {
$ret .= $point['lon'] . ',' . $point['lat'] . ',' . $point['alt'] . " ";
}
$ret .= '</coordinates></LinearRing></outerBoundaryIs></Polygon>';
}
return $ret;
}
function headerKML() {
$kml = <<<HEREDOC
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>line-of-sight.php</name>
<open>1</open>
<Style id="sh_placemark_circle_highlight">
<IconStyle>
<scale>1.7</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/shapes/placemark_circle_highlight.png</href>
</Icon>
</IconStyle>
<BalloonStyle id="balloon">
<bgColor>ff6c3adf</bgColor>
<textColor>ff000000</textColor>
<text>$[description]</text>
<displayMode>default</displayMode>
</BalloonStyle>
</Style>
<Style id="sn_placemark_circle">
<IconStyle>
<scale>1.3</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png</href>
</Icon>
</IconStyle>
<BalloonStyle id="balloon">
<bgColor>ff6c3adf</bgColor>
<textColor>ff000000</textColor>
<text>$[description]</text>
<displayMode>default</displayMode>
</BalloonStyle>
</Style>
<StyleMap id="msn_placemark_circle">
<Pair>
<key>normal</key>
<styleUrl>#sn_placemark_circle</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#sh_placemark_circle_highlight</styleUrl>
</Pair>
</StyleMap>
<Style id="line">
<LineStyle>
<width>1</width>
<color>ff000000</color>
</LineStyle>
<PolyStyle>
<color>a0ffffff</color>
</PolyStyle>
<BalloonStyle id="balloon">
<bgColor>ff6c3adf</bgColor>
<textColor>ff000000</textColor>
<text>$[description]</text>
<displayMode>default</displayMode>
</BalloonStyle>
</Style>
<Style id="polygon">
<LineStyle>
<width>1</width>
<color>ff006000</color>
</LineStyle>
<PolyStyle>
<color>ff50ff50</color>
</PolyStyle>
<BalloonStyle id="balloon">
<bgColor>ff6c3adf</bgColor>
<textColor>ff000000</textColor>
<text>$[description]</text>
<displayMode>default</displayMode>
</BalloonStyle>
</Style>
<Style id="polygon-transparent">
<LineStyle>
<width>1</width>
<color>4000ff00</color>
</LineStyle>
<PolyStyle>
<color>8000ff00</color>
</PolyStyle>
<BalloonStyle id="balloon">
<bgColor>ff6c3adf</bgColor>
<textColor>ff000000</textColor>
<text>$[description]</text>
<displayMode>default</displayMode>
</BalloonStyle>
</Style>
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 = <<<HEREDOC
<style type="text/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;}
</style>
HEREDOC;
return $css;
}
?>