From b3b6d0750030d08032756ad4a6969e193a65a928 Mon Sep 17 00:00:00 2001 From: Ivan Kohler Date: Fri, 26 Aug 2022 12:23:43 -0700 Subject: [PATCH] add shapefile, kmz and geojson export to deployment zones, RT#86460 --- FS/FS/Mason.pm | 6 ++- FS/FS/deploy_zone.pm | 71 ++++++++++++++++++++++++++++++ debian/control | 6 ++- httemplate/browse/deploy_zone.html | 63 ++++++++++++++++++++++++--- httemplate/view/deploy_zone-geojson.cgi | 42 ++++++++++++++++++ httemplate/view/deploy_zone-kmz.cgi | 42 ++++++++++++++++++ httemplate/view/deploy_zone-shp.cgi | 76 +++++++++++++++++++++++++++++++++ 7 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 httemplate/view/deploy_zone-geojson.cgi create mode 100644 httemplate/view/deploy_zone-kmz.cgi create mode 100644 httemplate/view/deploy_zone-shp.cgi diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 8dd72ac79..ebd40adda 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -85,6 +85,7 @@ if ( -e $addl_handler_use_file ) { die $@ if $@; } use Text::CSV_XS; + use Archive::Zip; use Spreadsheet::WriteExcel; use Spreadsheet::WriteExcel::Utility; use OLE::Storage_Lite; @@ -120,7 +121,10 @@ if ( -e $addl_handler_use_file ) { use Locale::Currency::Format; use Number::Phone::Country qw( noexport ); use Business::US::USPS::WebTools::AddressStandardization; - use Geo::GoogleEarth::Pluggable; + use Geo::GoogleEarth::Pluggable 0.16; + use Geo::Shapelib; + use Geo::JSON; + use Geo::JSON::FeatureCollection; use LWP::UserAgent; use Storable qw( nfreeze thaw ); use FS; diff --git a/FS/FS/deploy_zone.pm b/FS/FS/deploy_zone.pm index 723b491c8..16ba5ddf3 100644 --- a/FS/FS/deploy_zone.pm +++ b/FS/FS/deploy_zone.pm @@ -10,9 +10,14 @@ use Cpanel::JSON::XS; use LWP::UserAgent; use HTTP::Request::Common; +use Geo::JSON::Polygon; +use Geo::JSON::Feature; + # update this in 2020, along with the URL for the TIGERweb service our $CENSUS_YEAR = 2010; +our $tech_label = FS::part_pkg_fcc_option->technology_labels; + =head1 NAME FS::deploy_zone - Object methods for deploy_zone records @@ -275,6 +280,27 @@ sub deploy_zone_vertex { }); } +=item shapefile_add SHAPEFILE + +Adds this deployment zone to the supplied Geo::Shapelib shapefile. + +=cut + +sub shapefile_add { + my( $self, $shapefile ) = @_; + + my @coordinates = map { [ $_->longitude, $_->latitude, 0, 0 ] } + $self->deploy_zone_vertex; + push @coordinates, $coordinates[0]; + + push @{$shapefile->{Shapes}}, { 'Vertices' => \@coordinates }; + push @{$shapefile->{ShapeRecords}}, [ $tech_label->{$self->technology}, + $self->adv_speed_down, + $self->adv_speed_up, + ]; + ''; +} + =item vertices_json Returns the vertex list for this zone, as a JSON string of @@ -289,6 +315,51 @@ sub vertices_json { encode_json(\@vertices); } +=item geo_json_feature + +Returns this zone as a Geo::JSON::Feature object + +=cut + +sub geo_json_feature { + my $self = shift; + + my @coordinates = map { [ $_->longitude, $_->latitude ] } + $self->deploy_zone_vertex; + push @coordinates, $coordinates[0]; + + Geo::JSON::Feature->new({ + geometry => Geo::JSON::Polygon->new({ coordinates => [ \@coordinates ] }), + properties => { 'Technology' => $tech_label->{$self->technology}, + 'Down' => $self->adv_speed_down, + 'Up' => $self->adv_speed_up, + }, + }) +} + +=item kml_add + +Adds this deployment zone to the supplied Geo::GoogleEarth::Pluggable object. + +=cut + +sub kml_polygon { + my( $self, $kml ) = @_; + + my $name = $self->description. ' ('. $self->adv_speed_down. '/'. + $self->adv_speed_up. ')'; + + $kml->Polygon( 'name' => $name, + 'coordinates' => [ [ #outerBoundary + map { [ $_->longitude, $_->latitude, 0 ] } + $self->deploy_zone_vertex + ], + #[ #innerBoundary + #] + ] + ); +} + =head2 SUBROUTINES =over 4 diff --git a/debian/control b/debian/control index 0f1e29fb9..479a150d1 100644 --- a/debian/control +++ b/debian/control @@ -78,7 +78,8 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip, libipc-run-safehandles-perl,libpoe-perl,libsoap-lite-perl,libxmlrpc-lite-perl, libhtml-tableextract-perl,libhtml-element-extended-perl,libcam-pdf-perl, libnet-openssh-perl,libgd-barcode-perl,sam2p,libsys-sigaction-perl, - libgeo-googleearth-pluggable-perl,libgeo-coder-googlev3-perl,libnet-snmp-perl, + libgeo-googleearth-pluggable-perl (>=0.16),libgeo-coder-googlev3-perl, + libnet-snmp-perl, libcrypt-openssl-rsa-perl,libregexp-common-perl,libnet-cidr-perl, libregexp-ipv6-perl,libhtml-quoted-perl,libtext-password-pronounceable-perl, libconvert-color-perl,liburi-perl,libhtml-rewriteattributes-perl, @@ -107,7 +108,8 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip, libspreadsheet-parsexlsx-perl, libunicode-truncate-perl (>= 0.303-1), libspreadsheet-xlsx-perl, libpod-simple-perl, libwebservice-northern911-perl, liblocale-codes-perl, liblocale-po-perl, libgeo-uscensus-geocoding-perl, - libnet-sftp-foreign-perl, libpdf-webkit-perl + libnet-sftp-foreign-perl, libpdf-webkit-perl, libgeo-shapelib-perl, + libgeo-json-perl Conflicts: libparams-classify-perl (>= 0.013-6) Replaces: freeside (<<4) Breaks: freeside (<<4) diff --git a/httemplate/browse/deploy_zone.html b/httemplate/browse/deploy_zone.html index 5514d7db8..33724e59f 100644 --- a/httemplate/browse/deploy_zone.html +++ b/httemplate/browse/deploy_zone.html @@ -19,6 +19,21 @@ 'Contractual Mbps', 'Vertices', 'Census blocks', + 'Shapefile', + 'KMZ', + 'GeoJSON', + ], + footer => [ '', + 'All fixed zones', + '', + '', + '', + '', + '', + '', + 'download', + 'download', + 'download', ], fields => [ 'zonenum', 'description', @@ -48,6 +63,18 @@ sub { my $self = shift; FS::deploy_zone_block->count('zonenum = '.$self->zonenum) }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, ], sort_fields => [ 'zonenum', 'description', @@ -56,7 +83,7 @@ '(adv_speed_down, adv_speed_up)', '(cir_speed_down, cir_speed_up)', ], - links => [ $link_fixed, $link_fixed, ], + links => [ $link_fixed, $link_fixed, '', '', '', '', '', '', $link_shp, $link_kmz, $link_json, ], align => 'cllllrrr', nohtmlheader => 1, disable_maxselect => 1, @@ -79,6 +106,9 @@ 'Service Type', 'Advertised Mbps', 'Vertices', # number of vertices? not so useful + 'Shapefile', + 'KMZ', + 'GeoJSON', ], fields => [ 'zonenum', 'description', @@ -101,6 +131,18 @@ sub { my $self = shift; FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, ], sort_fields => [ 'zonenum', 'description', @@ -109,7 +151,7 @@ '(is_voice is not null, is_broadband is not null)', '(adv_speed_down, adv_speed_up)', ], - links => [ '', $link_mobile, ], + links => [ $link_mobile, $link_mobile, '', '', '', '', '', '', $link_shp, $link_kmz, $link_json, ], align => 'clllllr', nohtmlheader => 1, disable_maxselect => 1, @@ -120,15 +162,24 @@ <& /elements/footer.html &> <%init> + my $curuser = $FS::CurrentUser::CurrentUser; my $acl_edit = $curuser->access_right('Edit FCC report configuration'); my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents'); die "access denied" unless $acl_edit or $acl_edit_global; -my $link_fixed = [ $p.'edit/deploy_zone-fixed.html?', 'zonenum' ]; -my $link_mobile= [ $p.'edit/deploy_zone-mobile.html?', 'zonenum' ]; +my $link_fixed = [ $p.'edit/deploy_zone-fixed.html?', 'zonenum' ]; +my $link_mobile = [ $p.'edit/deploy_zone-mobile.html?', 'zonenum' ]; +my $link_shp = [ $p.'view/deploy_zone-shp.cgi?', 'zonenum' ]; +my $link_kmz = [ $p.'view/deploy_zone-kmz.cgi?', 'zonenum' ]; +my $link_json = [ $p.'view/deploy_zone-geojson.cgi?', 'zonenum' ]; + +my $fixed_shp = $p.'view/deploy_zone-shp.cgi?zonetype=B'; +my $fixed_kmz = $p.'view/deploy_zone-kmz.cgi?zonetype=B'; +my $fixed_json = $p.'view/deploy_zone-geojson.cgi?zonetype=B'; + +my $tech_label = FS::part_pkg_fcc_option->technology_labels; +my $spec_label = FS::part_pkg_fcc_option->spectrum_labels; -my $tech_label = FS::part_pkg_fcc_option->technology_labels; -my $spec_label = FS::part_pkg_fcc_option->spectrum_labels; diff --git a/httemplate/view/deploy_zone-geojson.cgi b/httemplate/view/deploy_zone-geojson.cgi new file mode 100644 index 000000000..0c9d19353 --- /dev/null +++ b/httemplate/view/deploy_zone-geojson.cgi @@ -0,0 +1,42 @@ +<% $content %>\ +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +my $acl_edit = $curuser->access_right('Edit FCC report configuration'); +my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents'); +die "access denied" + unless $acl_edit or $acl_edit_global; + +my($name, $content); + +my($query) = $cgi->keywords; +if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) { + my $zonenum = $1; + $name = $zonenum; + my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum }) + or die 'unknown zonenum'; + + $content = $deploy_zone->geo_json_feature->to_json; + +} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) { + my $zonetype = $1; + $name = $zonetype; + my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype, + 'disabled' => '', }); + + my $fc = Geo::JSON::FeatureCollection->new({ + features => [ map $_->geo_json_feature, @deploy_zone ], + }); + + $content = $fc->to_json; + +} else { + die "no zonenum or zonetype\n"; +} + +http_header('Content-Type' => 'application/geo+json' ); +http_header('Content-Disposition' => "filename=$name.geojson" ); +http_header('Content-Length' => length($content) ); +http_header('Cache-control' => 'max-age=60' ); + + diff --git a/httemplate/view/deploy_zone-kmz.cgi b/httemplate/view/deploy_zone-kmz.cgi new file mode 100644 index 000000000..d2af171e5 --- /dev/null +++ b/httemplate/view/deploy_zone-kmz.cgi @@ -0,0 +1,42 @@ +<% $content %>\ +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +my $acl_edit = $curuser->access_right('Edit FCC report configuration'); +my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents'); +die "access denied" + unless $acl_edit or $acl_edit_global; + +my $kml = Geo::GoogleEarth::Pluggable->new; + +my $name; + +my($query) = $cgi->keywords; +if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) { + my $zonenum = $1; + $name = $zonenum; + my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum }) + or die 'unknown zonenum'; + + $deploy_zone->kml_polygon($kml); + +} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) { + my $zonetype = $1; + $name = $zonetype; + my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype, + 'disabled' => '', }); + + $_->kml_polygon($kml) foreach @deploy_zone; + +} else { + die "no zonenum or zonetype\n"; +} + +my $content = $kml->archive; + +http_header('Content-Type' => 'application/vnd.google-earth.kmz' ); #kmz +http_header('Content-Disposition' => "filename=$name.kmz" ); +http_header('Content-Length' => length($content) ); +http_header('Cache-control' => 'max-age=60' ); + + diff --git a/httemplate/view/deploy_zone-shp.cgi b/httemplate/view/deploy_zone-shp.cgi new file mode 100644 index 000000000..4b5dfbbc6 --- /dev/null +++ b/httemplate/view/deploy_zone-shp.cgi @@ -0,0 +1,76 @@ +<% $content %>\ +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +my $acl_edit = $curuser->access_right('Edit FCC report configuration'); +my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents'); +die "access denied" + unless $acl_edit or $acl_edit_global; + +my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; + +my %shapelib_opts = ( + Shapetype => Geo::Shapelib::POLYGON, + FieldNames => [ 'Tech', 'Down', 'Up' ], + FieldTypes => [ 'String:32', 'Double', 'Double' ], +); + +my( $name, $shapefile ); + +my($query) = $cgi->keywords; +if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) { + my $zonenum = $1; + $name = $zonenum; + my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum }) + or die 'unknown zonenum'; + + $shapefile = new Geo::Shapelib { + Name => "$dir/$zonenum-$$", + %shapelib_opts + }; + + $deploy_zone->shapefile_add($shapefile); + +} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) { + my $zonetype = $1; + $name = $zonetype; + my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype, + 'disabled' => '', }); + + $shapefile = new Geo::Shapelib { + Name => "$dir/$zonetype-$$", + %shapelib_opts + }; + + $_->shapefile_add($shapefile) foreach @deploy_zone; + +} else { + die "no zonenum or zonetype\n"; +} + +$shapefile->set_bounds; + +$shapefile->save; + +#slurp up .shp .shx and .dbf files and put them in a zip.. return that +#and delete the files + +my $content = ''; +open(my $fh, '>', \$content); + +my $zip = new Archive::Zip; +$zip->addFile("$dir/$name-$$.$_", "$name.$_") foreach qw( shp shx dbf ); +unless ( $zip->writeToFileHandle($fh) == Archive::Zip::AZ_OK() ) { + die "failed to create .shz file\n"; +} +close $fh; + +unlink("$dir/$name-$$.$_") foreach qw( shp shx dbf ); + +#http_header('Content-Type' => 'x-gis/x-shapefile' ); +http_header('Content-Type' => 'archive/zip' ); +http_header('Content-Disposition' => "filename=$name.shz" ); +http_header('Content-Length' => length($content) ); +http_header('Cache-control' => 'max-age=60' ); + + -- 2.11.0