diff options
author | Mark Wells <mark@freeside.biz> | 2016-10-10 11:59:41 -0700 |
---|---|---|
committer | Mark Wells <mark@freeside.biz> | 2016-10-10 11:59:41 -0700 |
commit | 49d9ea969069430ef3fe23e5b1ac3599e929bb04 (patch) | |
tree | 24a3feb13b0a8db68f7a634de239b97d106a5efe /httemplate/search | |
parent | 53a8c81b4f3a414803a52fc8114b26a71055d012 (diff) |
new tower/sector UI, mapping features, and network monitoring, #37802
Diffstat (limited to 'httemplate/search')
-rw-r--r-- | httemplate/search/elements/gmap.html | 63 | ||||
-rw-r--r-- | httemplate/search/sector.html | 1 | ||||
-rwxr-xr-x | httemplate/search/svc_broadband-json.cgi | 108 | ||||
-rwxr-xr-x | httemplate/search/svc_broadband-map.html | 35 | ||||
-rwxr-xr-x | httemplate/search/tower-map.html | 303 |
5 files changed, 469 insertions, 41 deletions
diff --git a/httemplate/search/elements/gmap.html b/httemplate/search/elements/gmap.html index b7d135dd6..69fdc5a09 100644 --- a/httemplate/search/elements/gmap.html +++ b/httemplate/search/elements/gmap.html @@ -37,6 +37,9 @@ Generic Google Maps front end. </%doc> <%init> + +my $apikey = FS::Conf->new->config('google_maps_api_key'); + foreach (@features) { $_->{type} = 'Feature'; # any other per-feature massaging can go here @@ -57,7 +60,7 @@ body { height: 100%; margin: 0px; padding: 0px } #map_canvas { height: 100%; } </style> -<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3"> +<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3&key=<% $apikey %>"> </script> <script type="text/javascript"> @@ -86,7 +89,42 @@ var featureStyle = function(feature) { var map; var overlays = []; -function initMap() { +var infoWindow; // shared among all users + +var clickHandler = function(ev) { + var feature = ev.feature; + if ( feature.getGeometry().getType() == 'Point' ) { + // then pop up an info box with the feature content + infoWindow.close(); + infoWindow.setPosition(feature.getGeometry().get()); + + if ( feature.getProperty('content') ) { + infoWindow.setContent(feature.getProperty('content')); + } else { + infoWindow.setContent(''); + } + + if ( feature.getProperty('url') ) { + $.ajax({ + url: feature.getProperty('url'), + success: function(data) { + infoWindow.setContent(data); + } + }); + infoWindow.open(map); + } else { + infoWindow.open(map); + } + } + + // snap to feature ROI if it has one + if ( feature.getProperty('bounds') ) { + map.fitBounds( feature.getProperty('bounds') ); + } + +}; + +var initMap = function() { var canvas = $('#map_canvas'); map = new google.maps.Map(canvas[0], { zoom: 6 }); try { @@ -110,24 +148,9 @@ function initMap() { map.fitBounds(bounds); map.data.setStyle(featureStyle); - var info = new google.maps.InfoWindow; - map.data.addListener('click', function(ev) { - var feature = ev.feature; - if ( feature.getGeometry().getType() == 'Point' ) { - // then pop up an info box with the feature content - info.close(); - info.setPosition(feature.getGeometry().get()); - info.setContent(feature.getProperty('content')); - info.open(map); - } - - // snap to feature ROI if it has one - if ( feature.getProperty('bounds') ) { - map.fitBounds( feature.getProperty('bounds') ); - } - - }); // addListener() - + infoWindow = new google.maps.InfoWindow; + map.data.addListener('click', clickHandler); + // xxx remove this later data_overlays.forEach(function(x) { var url = x.url; delete x.url; diff --git a/httemplate/search/sector.html b/httemplate/search/sector.html index 037df10ea..636935489 100644 --- a/httemplate/search/sector.html +++ b/httemplate/search/sector.html @@ -71,6 +71,7 @@ <a class="createmap" href="#" onclick="sector<% $sectornum %>process()"> <% $text %> </a> + </form> % } </td> <td> diff --git a/httemplate/search/svc_broadband-json.cgi b/httemplate/search/svc_broadband-json.cgi new file mode 100755 index 000000000..334e1ef3c --- /dev/null +++ b/httemplate/search/svc_broadband-json.cgi @@ -0,0 +1,108 @@ +<% encode_json({ + type => 'FeatureCollection', + features => \@features +}) %> +<%init> + +die "access denied" unless + $FS::CurrentUser::CurrentUser->access_right('List services'); + +my $conf = new FS::Conf; + +my @features; # geoJSON structure + +# accept all the search logic from svc_broadband.cgi... +my %search_hash; +if ( $cgi->param('magic') eq 'unlinked' ) { + %search_hash = ( 'unlinked' => 1 ); +} else { + foreach (qw( custnum agentnum svcpart cust_fields )) { + $search_hash{$_} = $cgi->param($_) if $cgi->param($_); + } + foreach (qw(pkgpart routernum towernum sectornum)) { + $search_hash{$_} = [ $cgi->param($_) ] if $cgi->param($_); + } +} + +if ( $cgi->param('sortby') =~ /^(\w+)$/ ) { + $search_hash{'order_by'} = "ORDER BY $1"; +} + +my $sql_query = FS::svc_broadband->search(\%search_hash); + +my %routerbyblock = (); + +my @rows = qsearch($sql_query); +my %sectors; +my %towers; +my %tower_coord; + +foreach my $svc_broadband (@rows) { + # don't try to show it if coords aren't set + next if !$svc_broadband->latitude || !$svc_broadband->longitude; + # coerce coordinates to numbers + my @coord = ( + $svc_broadband->longitude + 0, + $svc_broadband->latitude + 0, + ); + push @coord, $svc_broadband->altitude + 0 + if length($svc_broadband->altitude); # it's optional + + my $svcnum = $svc_broadband->svcnum; + my $color = $svc_broadband->addr_status_color; + + push @features, + { + type => 'Feature', + id => 'svc_broadband/'.$svcnum, + geometry => { + type => 'Point', + coordinates => \@coord, + }, + properties => { + #content => include('.svc_broadband', $svc_broadband), + url => $fsurl . 'view/svc_broadband-popup.html?' . $svcnum, + style => { + icon => { + fillColor => $color, + }, + }, + }, + }; + # look up tower location and draw connecting line + next if !$svc_broadband->sectornum; + my $sector = $sectors{$svc_broadband->sectornum} ||= $svc_broadband->tower_sector; + my $towernum = $sector->towernum; + my $tower = $towers{$towernum}; + + if (!$tower) { + $tower = $towers{$towernum} = $sector->tower; + $tower_coord{$towernum} = + [ $tower->longitude + 0, + $tower->latitude + 0, + ($tower->altitude || 0) + 0, + ]; + + } + + if ( $tower->latitude and $tower->longitude ) { + push @features, + { + type => 'Feature', + id => 'svc_broadband/'.$svcnum.'/line', + geometry => { + type => 'LineString', + coordinates => [ \@coord, $tower_coord{$towernum} ], + }, + properties => { + style => { + visible => 0, + strokeColor => $color, + strokeWeight => 2, + }, + }, + }; + + } # if tower has coords +} # foreach $svc_broadband +</%init> diff --git a/httemplate/search/svc_broadband-map.html b/httemplate/search/svc_broadband-map.html index fe3c0950b..41f4b8dfd 100755 --- a/httemplate/search/svc_broadband-map.html +++ b/httemplate/search/svc_broadband-map.html @@ -49,15 +49,24 @@ foreach my $svc_broadband (@rows) { push @coord, $svc_broadband->altitude + 0 if length($svc_broadband->altitude); # it's optional + my $svcnum = $svc_broadband->svcnum; + my $color = $svc_broadband->addr_status_color; + push @features, { - id => 'svc_broadband/'.$svc_broadband->svcnum, + id => 'svc_broadband/'.$svcnum, geometry => { type => 'Point', coordinates => \@coord, }, properties => { - content => include('.svc_broadband', $svc_broadband), + #content => include('.svc_broadband', $svc_broadband), + url => $fsurl . 'view/svc_broadband-popup.html?' . $svcnum, + style => { + icon => { + fillColor => $color, + }, + }, }, }; # look up tower location and draw connecting line @@ -85,8 +94,8 @@ foreach my $svc_broadband (@rows) { }, properties => { style => { - strokeColor => ($tower->color || 'green'), - strokeWeight => 2, + strokeColor => $color, + strokeWeight => 1, }, }, }; @@ -135,7 +144,7 @@ foreach my $tower (values(%towers)) { style => { icon => { path => undef, - url => $fsurl.'images/jcartier-antenna-square-21x51.png', + url => $fsurl.'images/antenna-square-21x51.png', anchor => { x => 10, y => 4 } }, }, @@ -159,22 +168,6 @@ foreach my $sector (values %sectors) { }; </%init> -<%def .svc_broadband> -% my $svc = shift; -% my @label = $svc->cust_svc->label; -<H3> - <a target="_blank" href="<% $fsurl %>view/svc_broadband.cgi?<% $svc->svcnum %>"> - <% $label[0] |h %> #<% $svc->svcnum %> | <% $label[1] %> - </a> -</H3> -% my $cust_main = $svc->cust_main; -<a target="_blank" href="<% $fsurl %>view/cust_main.cgi?<% $cust_main->custnum %>"> -<& /elements/small_custview.html, { - cust_main => $svc->cust_main, - #url => $fsurl.'view/cust_main.cgi', -} &> -</a> -</%def> <%def .tower> % my $tower = shift; % my $can_edit = $FS::CurrentUser::CurrentUser->access_right('Configuration'); diff --git a/httemplate/search/tower-map.html b/httemplate/search/tower-map.html new file mode 100755 index 000000000..559d83d08 --- /dev/null +++ b/httemplate/search/tower-map.html @@ -0,0 +1,303 @@ +<& /elements/header.html, '' &> + +<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3&libraries=places&key=<% $apikey %>"></script> + +<style> +html { height: 100% } +#map_canvas { margin: 0 auto; height: 100%; } +span.is_up { font-weight: bold; color: green } +span.is_down { font-weight: bold; color: red } +#search_location { width: 300px } +</style> + +<div id="map_canvas"></div> +<input type="text" id="search_location" style="width: 180px"> + + +<script type="text/javascript"> + +var baseMarkerStyle = { + clickable: true, + icon: { + path: google.maps.SymbolPath.CIRCLE, + scale: 4, + fillColor: 'black', + fillOpacity: 1, + strokeColor: 'black', + strokeWeight: 1, + }, +}; + +var baseCoverageStyle = { + clickable: false, + strokeWeight: 0.2, +}; + +var coverageStyle = function(feature) { + var s = $.extend(true, {}, baseCoverageStyle, feature.getProperty('style')); + if ( feature.getProperty('low') ) { + s.fillOpacity = 0.1; + } else if ( feature.getProperty('high') ) { + s.fillOpacity = 0.4; + } + return s; +} + +var markerStyle = function(feature) { + return $.extend(true, {}, baseMarkerStyle, feature.getProperty('style')); +} + +var map; +var infoWindow = new google.maps.InfoWindow; // shared among all users + +var clickHandler = function(ev) { + var feature = ev.feature; + if ( feature.getGeometry().getType() == 'Point' ) { + // then pop up an info box with the feature content + infoWindow.close(); + infoWindow.setPosition(feature.getGeometry().get()); + + if ( feature.getProperty('content') ) { + infoWindow.setContent(feature.getProperty('content')); + } else { + infoWindow.setContent(''); + } + + if ( feature.getProperty('url') ) { + $.ajax({ + url: feature.getProperty('url'), + success: function(data) { + infoWindow.setContent(data); + } + }); + infoWindow.open(map); + } else { + infoWindow.open(map); + } + } +}; + +var dblclickHandler = function(ev) { + // do everything as for single click + clickHandler(ev); + // plus zoom to the feature + var feature = ev.feature; + if (feature.getGeometry().getType() == 'Point') { + map.setCenter(feature.getGeometry().get()); + map.setZoom(12); + } +}; + +var zoomLayer = function(layer) { + // takes a google.maps.Data object + var bounds = new google.maps.LatLngBounds; + layer.forEach(function(feature) { + var g = feature.getGeometry(); + if (g.getType() == 'Point') { + bounds.extend(g.get()); + } else if (g.getArray) { + g.getArray().forEach(function(point) { bounds.extend(point); }); + } + }); + + map.fitBounds(bounds); +}; + +// set up the main layer +var tower_data = new google.maps.Data; +tower_data.addGeoJson(<% encode_json($tower_data) %>); +tower_data.setStyle(markerStyle); +tower_data.addListener('click', clickHandler); +tower_data.addListener('dblclick', dblclickHandler); + +var towernums = <% encode_json(\@towernums) %>; +var tower_svc_data = {}; +var tower_coverage_data = {}; + +var revertLayerStyles = function() { + // mostly, just to re-hide all connecting lines when something is hidden + for (var t in tower_svc_data) { + tower_svc_data[t].revertStyle(); + } +}; + +towernums.forEach(function(towernum) { + var layer = new google.maps.Data; + tower_svc_data[towernum] = layer; + layer.loadGeoJson( + '<% $fsurl %>search/svc_broadband-json.cgi?towernum=' + towernum + ); + layer.setStyle(markerStyle); + layer.addListener('click', clickHandler); + layer.addListener('click', function(ev) { // show connecting line + var id = ev.feature.getId(); + var f_line = layer.getFeatureById(id + '/line'); + layer.overrideStyle(f_line, { visible: true}); + }); + + layer = new google.maps.Data; + layer.loadGeoJson( + '<% $fsurl %>misc/sector_coverage-json.cgi?towernum=' + towernum + ); + layer.setStyle(coverageStyle); + tower_coverage_data[towernum] = layer; +}); + +function show_svc_data(towernum, show) { + if (show) { + tower_svc_data[towernum].setMap(window.map); + } else { + tower_svc_data[towernum].setMap(null); + } +}; + +function show_coverage_data(towernum, show) { + if (show) { + tower_coverage_data[towernum].setMap(window.map); + } else { + tower_coverage_data[towernum].setMap(null); + } +}; + +// toggle visibility of the services +infoWindow.addListener('domready', function(ev) { + var show_services_box = $('input[name=show_services]'); + var towernum = show_services_box.val(); + var is_shown = tower_svc_data[towernum].getMap() == map; + show_services_box.prop('checked', is_shown); + show_services_box.on('click', function(clickev) { + show_svc_data(towernum, this.checked); + }); + + var show_coverage_box = $('input[name=show_coverage]'); + var towernum = show_coverage_box.val(); + var is_shown = tower_coverage_data[towernum].getMap() == map; + show_coverage_box.prop('checked', is_shown); + show_coverage_box.on('click', function(clickev) { + show_coverage_data(towernum, this.checked); + }); +}); + +infoWindow.addListener('closeclick', revertLayerStyles); +infoWindow.addListener('position_changed', revertLayerStyles); + +var initMap = function() { + var canvas = $('#map_canvas'); + + // set window height correctly + canvas.css('height', window.innerHeight - (canvas.offset().top) - 30); + canvas.css('width', window.innerWidth - 30); + + map = new google.maps.Map(canvas[0], { zoom: 6 }); + + //set up search box + var searchbox_input = $('#search_location')[0]; + var searchbox = new google.maps.places.SearchBox(searchbox_input); + map.controls[google.maps.ControlPosition.TOP_RIGHT].push(searchbox_input); + + map.addListener('bounds_changed', function() { + searchbox.setBounds(map.getBounds()); + }); + + searchbox.addListener('places_changed', function() { + var places = searchbox.getPlaces(); + // xxx fancy mode: find the nearest tower and estimate signal strength + if (places[0]) { + if (places[0].geometry.viewport) { + map.fitBounds(places[0].geometry.viewport); + } else { + map.setCenter(places[0].geometry.location); + map.setZoom(14); + } + } + }); + + // put tower locations on map + tower_data.setMap(map); + zoomLayer(tower_data); +}; + +$().ready(initMap); + +</script> + +<& /elements/footer.html &> +<%init> + +die "access denied" unless + $FS::CurrentUser::CurrentUser->access_right('List services'); + +my $conf = new FS::Conf; + +my $apikey = $conf->config('google_maps_api_key'); + +my @features; # geoJSON structure + +my @towers = qsearch('tower', { + 'latitude' => { op=>'!=', value=>''}, + 'longitude' => { op=>'!=', value=>''}, +}); +my %sectors; # towernum => arrayref +my @towernums; + +foreach my $tower (@towers) { + my $towernum = $tower->towernum; + push @towernums, $towernum; + my @coord = ( + $tower->longitude + 0, + $tower->latitude + 0, + ); + push @features, + { + type => 'Feature', + id => 'tower/'.$towernum, + geometry => { + type => 'Point', + coordinates => \@coord, + }, + properties => { + style => { + icon => { + path => undef, + url => $fsurl.'images/antenna-square-21x51.png', + anchor => { x => 10, y => 4 }, + strokeColor => ($tower->color || 'black'), + }, + }, + content => include('.tower', $tower), + }, + }; + + $sectors{$towernum} = [ $tower->tower_sector ]; + +} # foreach $tower + +my $tower_data = { + type => 'FeatureCollection', + features => \@features +}; + +</%init> +<%def .tower> +% my $tower = shift; +% my $can_edit = $FS::CurrentUser::CurrentUser->access_right('Configuration'); +<H3> +% if ( $can_edit ) { + <a target="_blank" href="<% $fsurl %>edit/tower.html?<% $tower->towernum %>"> +% } +Tower #<% $tower->towernum %> | <% $tower->towername %> +% if ( $can_edit ) { + </a> +% } +</H3> +% my $count_query = 'SELECT COUNT(*) FROM svc_broadband LEFT JOIN addr_status using (ip_addr) JOIN tower_sector USING (sectornum) WHERE tower_sector.towernum = '.$tower->towernum; +% my $num_down = FS::Record->scalar_sql("$count_query AND addr_status.up IS NULL AND addr_status._date IS NOT NULL"); +% my $num_up = FS::Record->scalar_sql("$count_query AND addr_status.up IS NOT NULL"); +<input type="checkbox" name="show_services" value="<% $tower->towernum %>"> +<% emt('Show services') %> +( <% $num_up %> <SPAN CLASS="is_up"><% emt('UP') %></SPAN> +<% $num_down %> <SPAN CLASS="is_down"><% emt('DOWN') %></SPAN> ) +<br> +<input type="checkbox" name="show_coverage" value="<% $tower->towernum %>"> +<% emt('Show coverage') %> +</%def> |