## @class Gtk2::Ex::Geo::Glue # @brief A class for managing geospatial layers # @author Copyright (c) Ari Jolma # @author This library is free software; you can redistribute it and/or modify # it under the same terms as Perl itself, either Perl version 5.8.5 or, # at your option, any later version of Perl 5 you may have available. package Gtk2::Ex::Geo::Glue; =pod =head1 NAME Gtk2::Ex::Geo::Glue - A class for managing geospatial layers The documentation of Gtk2::Ex::Geo is written in doxygen format. =cut #use strict; # causes "Variable not imported in some cases" ?? use warnings; use Scalar::Util qw(blessed); use Carp; use Glib qw/TRUE FALSE/; use Gtk2::Ex::Geo::Overlay; use Gtk2::Ex::Geo::Layer; use Gtk2::Ex::Geo::Dialogs qw/:all/; use Gtk2::Ex::Geo::TreeDumper; BEGIN { use Exporter 'import'; our @EXPORT = qw(); our @EXPORT_OK = qw(); our %EXPORT_TAGS = ( FIELDS => [ @EXPORT_OK, @EXPORT ] ); } ## @cmethod object new(%params) # @brief Constructor # # @param params named parameters: # - history [optional] a history file of user input # - resources [optional] a user preferences file # @return a new Glue object sub new { my $class = shift; my %params = @_; my $self = {}; bless($self, $class); $self->{folder} = $params{first_file_open_folder} if $params{first_file_open_folder}; ($self->{tree_view}, $self->{model}) = $self->create_layer_tree_view; ($self->{toolbar}, $self->{mode_button}, $self->{geometry_button}) = $self->create_toolbar; $self->{statusbar} = Gtk2::Statusbar->new(); $self->{entry} = Gtk2::Entry->new(); $self->{entry}->signal_connect( key_press_event => \&eval_entry, $self ); $self->{overlay} = $self->create_overlay(@_); $self->set_interaction_mode('Zoom'); ($self->{history}, $self->{history_file}) = $self->open_history($params{history}); ($self->{resources}, $self->{resources_file}) = $self->open_resources($params{resources}); my @buffer = ; pop @buffer unless $buffer[$#buffer] =~ /^\new(buffer => \@buffer)); return $self; } sub create_layer_tree_view { my($self) = @_; my @columns = qw /name type v a/; my %tooltips = ( name => 'name', type => 'type', v => 'visibility', a => 'alpha' ); my $model = Gtk2::TreeStore->new(qw/Glib::String Glib::String Glib::String Glib::String/); my $view = Gtk2::TreeView->new(); $view->set_model($model); my $i = 0; foreach my $column (@columns) { my $cell = Gtk2::CellRendererText->new; if ($column eq 'a') { $cell->set(editable => 1); $cell->signal_connect(edited => \&layer_list_edit, [$self, $column]); } my $col = Gtk2::TreeViewColumn->new_with_attributes($column, $cell, text => $i++); $view->append_column($col); } my $selection = $view->get_selection; $selection->set_mode('multiple'); # does not seem to work?!? $view->signal_connect ( cursor_changed => sub { my(undef, $self) = @_; my $layer = $self->get_selected_layer(); return if $self->{focused} and $self->{focused}->name eq $layer->name; $self->{focused}->lost_focus($self) if $self->{focused}; $layer->got_focus($self) if $layer; $self->{focused} = $layer; }, $self); $view->signal_connect ( motion_notify_event => sub { my($self, $event, $gis) = @_; $self->set_has_tooltip(0); my @res = $self->get_path_at_pos($event->x, $event->y); return unless $res[0] and defined $res[0]->to_string; return unless $res[1]; my $layer = $gis->{overlay}->get_layer_by_index($res[0]->to_string); my $column = $res[1]->get_title; my $tooltip = $tooltips{$column}.': '; for ($column) { $tooltip .= $layer->name if /^name/; $tooltip .= $layer->type('long') if /^type/; $tooltip .= $layer->visible ? 'visible' : 'hidden' if /^v/; $tooltip .= $layer->alpha if /^a/; } $self->set_tooltip_text($tooltip); $self->set_has_tooltip(1); }, $self) if Gtk2->CHECK_VERSION(2,12,0); $view->signal_connect ( leave_notify_event => sub { my($self) = @_; $self->set_has_tooltip(0); }) if Gtk2->CHECK_VERSION(2,12,0); $view->signal_connect ( button_press_event => \&layer_menu, $self ); return ($view, $model); } sub layer_list_edit { my($cell, $path, $new_value, $data) = @_; my($self, $column) = @$data; my $layer = $self->{overlay}->get_layer_by_index($path); return unless $layer; if ($column eq 'name') { $layer->name($new_value) unless $self->{overlay}->get_layer_by_name($new_value); } else { $layer->alpha($new_value); $self->update; $self->{overlay}->render; } } sub create_toolbar { my($self) = @_; my $toolbar = Gtk2::Toolbar->new(); my $button1 = Gtk2::ComboBox->new; my $renderer = Gtk2::CellRendererText->new; $button1->pack_start($renderer, TRUE); $button1->add_attribute($renderer, text => 0); my $model = Gtk2::ListStore->new('Glib::String'); $button1->set_model($model); for my $command ('Zoom','Pan','Select','Measure','Draw','Edit') { $model->set($model->append, 0, $command); } $button1->signal_connect(changed => \&_set_interaction_mode, $self); $button1->set_tooltip_text('Interaction mode') if Gtk2->CHECK_VERSION(2,12,0); $button1->show_all; my $button2 = Gtk2::ComboBox->new; $renderer = Gtk2::CellRendererText->new; $button2->pack_start($renderer, TRUE); $button2->add_attribute($renderer, text => 0); $model = Gtk2::ListStore->new('Glib::String'); $button2->set_model($model); for my $command ('Line','Path','Rectangle','Ellipse','Polygon') { $model->set($model->append, 0, $command); } $button2->signal_connect(changed => \&_set_interaction_geometry, $self); $button2->set_tooltip_text('Interaction geometry') if Gtk2->CHECK_VERSION(2,12,0); $button2->show_all; my $item = Gtk2::ToolItem->new; $item->add($button2); $toolbar->insert($item, 0); $item = Gtk2::ToolItem->new; $item->add($button1); $toolbar->insert($item, 0); my $button = Gtk2::ToolButton->new_from_stock('gtk-zoom-in'); my $tooltips = Gtk2::Tooltips->new; my $tip = 'Zoom in one tenth.'; $button->set_tooltip($tooltips, $tip, ''); $tooltips->set_tip($button, $tip); $tooltips->enable; $toolbar->insert($button, -1); $button->signal_connect('clicked', sub {$_[1]->{overlay}->zoom_in}, $self); $button->show_all; $button = Gtk2::ToolButton->new_from_stock('gtk-zoom-out'); $tooltips = Gtk2::Tooltips->new; $tip = 'Zoom out one tenth.'; $button->set_tooltip($tooltips, $tip, ''); $tooltips->set_tip($button, $tip); $tooltips->enable; $toolbar->insert($button, -1); $button->signal_connect('clicked', sub {$_[1]->{overlay}->zoom_out}, $self); $button->show_all; $button = Gtk2::ToolButton->new_from_stock('gtk-zoom-fit'); $tooltips = Gtk2::Tooltips->new; $tip = 'Zoom to all.'; $button->set_tooltip($tooltips, $tip, ''); $tooltips->set_tip($button, $tip); $tooltips->enable; $toolbar->insert($button, -1); $button->signal_connect('clicked', sub {$_[1]->{overlay}->zoom_to_all}, $self); $button->show_all; return ($toolbar, $button1, $button2); } sub _set_interaction_mode { my($combo, $self) = @_; my $model = $combo->get_model; my $a = $combo->get_active(); my $iter = $model->get_iter_from_string($a); my $mode = $model->get_value($iter); if ($mode eq 'Zoom') { $self->set_interaction_geometry('Rectangle'); $self->{geometry_button}->set_sensitive(0); } elsif ($mode eq 'Pan') { $self->set_interaction_geometry('Line'); $self->{geometry_button}->set_sensitive(0); } elsif ($mode eq 'Select') { $self->set_interaction_geometry('Rectangle'); $self->{geometry_button}->set_sensitive(1); } elsif ($mode eq 'Measure') { $self->set_interaction_geometry('Line'); $self->{geometry_button}->set_sensitive(1); } elsif ($mode eq 'Draw') { $self->set_interaction_geometry('Rectangle'); $self->{geometry_button}->set_sensitive(1); } elsif ($mode eq 'Edit') { $self->set_interaction_geometry('Line'); $self->{geometry_button}->set_sensitive(0); } else { $self->{geometry_button}->set_sensitive(1); } $self->{overlay}->{rubberband_mode} = lc($mode); } sub set_interaction_mode { my($self, $mode) = @_; my $model = $self->{mode_button}->get_model; $model->foreach(\&set_combo_to, [$self->{mode_button}, $mode]); } sub _set_interaction_geometry { my($combo, $self) = @_; my $model = $combo->get_model; my $a = $combo->get_active(); my $iter = $model->get_iter_from_string($a); my $geometry = $model->get_value($iter); $self->{overlay}->{rubberband_geometry} = lc($geometry); } sub set_interaction_geometry { my($self, $geometry) = @_; my $model = $self->{geometry_button}->get_model; $model->foreach(\&set_combo_to, [$self->{geometry_button}, $geometry]); } sub create_overlay { my($self, %params) = @_; my $overlay = Gtk2::Ex::Geo::Overlay->new(); my($menu, $menu_item_setup) = overlay_menu(); my %overlay_params = ( menu => $menu, menu_item_setup => $menu_item_setup, rubberband_mode => 'zoom', rubberband_geometry => 'rect', selecting => 'that_intersect', ); for my $key (keys %params) { if ($key =~ /^overlay:(\w+)/) { $overlay_params{$1} = $params{$key}; } } $overlay->my_inits( %overlay_params ); $overlay->signal_connect ( pixmap_ready => sub { my($overlay, $gis) = @_; my $layer = $self->get_selected_layer(); if ($layer) { my $gc = Gtk2::Gdk::GC->new($overlay->{pixmap}); $gc->set_rgb_fg_color(Gtk2::Gdk::Color->new(65535,0,0)); $layer->render_selection($gc, $overlay); } }, $self); $overlay->signal_connect ( new_selection => sub { my(undef, $gis) = @_; my $overlay = $gis->{overlay}; my $layer = $gis->get_selected_layer(); if ($layer) { if ($overlay->{selection}) { $layer->select($overlay->{selecting} => $overlay->{selection}); } else { $layer->select(); } $overlay->update_image; $layer->open_features_dialog($self, 1); } }, $self); $overlay->signal_connect ( motion_notify => \&show_information, $self ); return $overlay; } sub open_history { my($self, $filename) = @_; my $history; if ($filename) { my $mode = 0600; chmod $mode, $filename if -e $filename; if (open TMP, $filename) { my @history = ; CORE::close TMP; for (@history) { chomp $_; s/\r//; } $history = new Gtk2::Ex::Geo::History(\@history); } else { carp("$!: $filename (it will be created at exit)"); } } $history = Gtk2::Ex::Geo::History->new(['']) unless $history; return ($history, $filename); } sub open_resources { my($self, $filename) = @_; my %resources; if ($filename) { my $mode = 0600; chmod $mode, $filename if -e $filename; if (open TMP, $filename) { my $key = ''; while () { chomp $_; s/\r//; if (/^ /) { s/^ //; my @value = split /\t/; my $k = shift @value; $resources{$key}{$k} = [@value] if $k; } else { $key = $_; } } CORE::close TMP; } else { carp("$!: $filename (it will be created at exit)"); } } return (\%resources, $filename); } ## @method register_dialogs($dialogs) # @brief Extend the capabilities by adding new dialogs sub register_dialogs { my($self, $dialogs) = @_; croak "$dialogs is not a DialogMaster" unless $dialogs->isa('Gtk2::Ex::Geo::DialogMaster'); push @{$self->{dialogs}}, $dialogs; } sub register_command { my($self, %args) = @_; if (!$args{icon_widget} and $Config::Config{'osname'} ne 'MSWin32') { $args{icon_widget} = Gtk2::Label->new($args{label}); } my $button; if ($args{stock_id}) { $button = Gtk2::ToolButton->new_from_stock($args{stock_id}); } else { $button = Gtk2::ToolButton->new($args{icon_widget}, $args{label}); } $button->set_icon_name($args{icon_name}) if $args{icon_name}; $button->set_label_widget($args{label_widget}) if $args{label_widget}; my $tooltips = Gtk2::Tooltips->new; $args{tip} = $args{tip} || ''; $button->set_tooltip($tooltips, $args{tip}, ''); $tooltips->set_tip($button, $args{tip}); $tooltips->enable; $args{pos} = -1 unless defined $args{pos}; $self->{toolbar}->insert($button, $args{pos}); $button->signal_connect('clicked', $args{sub}, $self); $self->{buttons}{$args{tag}} = $button; $self->{commands}{$args{tag}} = $args{sub}; $button->show_all; } sub unregister_command { my($self, $tag) = @_; $self->{toolbar}->remove($self->{buttons}{$tag}) if $self->{buttons}{$tag}; delete $self->{buttons}{$tag}; delete $self->{commands}{$tag}; } ## @method register_commands($commands) # @brief Extend the capabilities by adding new commands sub register_commands { my($self, $commands) = @_; unless (ref $commands->[0]) { my @commands; while (ref $commands->[$#$commands]) { push @commands, pop @$commands; } my $menu = Gtk2::Menu->new; for my $command (reverse @commands) { my $name = $command->{label}; my $item; #$item = Gtk2::SeparatorMenuItem->new(); $item = Gtk2::MenuItem->new_with_label($name); $item->signal_connect(activate => $command->{sub}, $self); $menu->append($item); } my %args = @$commands; if (!$args{icon_widget} and $Config::Config{'osname'} ne 'MSWin32') { $args{icon_widget} = Gtk2::Label->new($args{label}); } my $button; if ($args{stock_id}) { $button = Gtk2::ToolButton->new_from_stock($args{stock_id}); } else { $button = Gtk2::ToolButton->new($args{icon_widget}, $args{label}); } $button->set_icon_name($args{icon_name}) if $args{icon_name}; $button->set_label_widget($args{label_widget}) if $args{label_widget}; my $tooltips = Gtk2::Tooltips->new; $args{tip} = $args{tip} || ''; $button->set_tooltip($tooltips, $args{tip}, ''); $tooltips->set_tip($button, $args{tip}); $tooltips->enable; $args{pos} = -1 unless defined $args{pos}; #$button->set_menu($menu); $button->show_all; $menu->show_all; $button->signal_connect(clicked => sub { $menu->popup(undef, undef, undef, undef, 0, 0); }); $self->{toolbar}->insert($button, $args{pos}); } else { for my $command (@$commands) { $self->register_command(%$command); } } } sub run_command { my($self, $command) = @_; $self->{commands}{$command}->(undef, $self); } ## @method object register_function(%params) # @brief Extend the capabilities by adding a new function sub register_function { my($self, %params) = @_; $self->{functions}{$params{name}} = \%params; } ## @method register_class(%params) # @brief Extend the capabilities # # @param params named parameters: # - class [optional] the name of the layer class. If this is # given, dialogs and commands are retrieved from the class with method # Gtk2::Ex::Geo::Layer::registration # - dialogs [optional] an object containing dialogs (a dialog master object) # - commands [optional] an anonymous hash of commands for the GUI # A command is defined in an anonymous hash with parameters: # - nr a visual order of the commands # - pos the pos parameter in toolbar->insert # - text the text for the command button # - tip the tip for the command button # - sub a reference to a subroutine to be executed sub register_class { my $self = shift; my %params; if (@_ > 1) { %params = @_; } else { $params{class} = shift; } if ($params{class}) { my $sub = $params{class}.'::upgrade'; push @{$self->{upgrades}}, \&$sub if defined &$sub; $sub = $params{class}.'::registration'; my $registration = &$sub($self); %params = %$registration; } $self->register_dialogs($params{dialogs}) if $params{dialogs}; $self->register_commands($params{commands}) if $params{commands}; } ## @ignore sub register_feature_class { my($self) = shift; for my $class (@_) { $self->{feature_classes}{$class} = $class; } } ## @method close # @brief Attempt to destroy all widgets in the GUI. sub close { my($self) = @_; if ($self->{history_file}) { my $history = $self->{history}->{history}; if (open TMP,">$self->{history_file}") { for (@$history[max(0,$#$history-1000)..$#$history]) { print TMP "$_\n"; } close TMP; } else { croak "$!: $self->{history_file}"; } } if ($self->{resources_file}) { my $resources = $self->{resources}; if (open TMP,">$self->{resources_file}") { for my $key (keys %$resources) { print TMP "$key\n"; for my $value (keys %{$resources->{$key}}) { print TMP " $value\t",join("\t",@{$resources->{$key}{$value}}),"\n"; } } close TMP; } else { croak "$!: $self->{resources_file}"; } } while ($self->delete_selected(1)) {}; delete $self->{dialogs}; delete $self->{commands}; delete $self->{functions}; $self->{overlay}->close; delete $self->{overlay}; delete $self->{model}; for my $key ('mode_button', 'geometry_button', 'toolbar', 'tree_view', 'entry', 'statusbar') { $self->{$key}->destroy; delete $self->{$key}; } while (my($key, $widget) = each %$self) { next if $key eq 'treedumper'; $widget->destroy if blessed($widget) and $widget->isa("Gtk2::Widget"); delete $self->{$key}; } } ## @method scalar get_dialog(name) # @brief Retrieve a dialog by its name. sub get_dialog { my($self, $dialog_name) = @_; for my $dialogs (@{$self->{dialogs}}) { $d = $dialogs->get_dialog($dialog_name); return $d if $d; } croak "can't find dialog $dialog_name"; } ## @method message($message) # @brief Display a short information message to the user. sub message { my($self, $message) = @_; my $dialog = Gtk2::MessageDialog->new(undef, 'destroy-with-parent', 'info', 'close', $message); $dialog->signal_connect(response => sub {$_[0]->destroy}); $dialog->show_all; } ## @fn overlay_menu() # @brief Construct a menu for an overlay object. sub overlay_menu { my @menu = ('Zoom to pre_vious' => sub { my ($item, $self) = @_; my $zoom = pop @{$self->{zoom_stack}}; $self->zoom(@$zoom, 0, 1) if $zoom; }, 1 => 0, 'Reselect' => sub { my ($item, $self) = @_; $self->signal_emit('new_selection'); }, '_Clear selection' => sub { my ($item, $self) = @_; if ($self->{selection}) { delete $self->{selection}; $self->signal_emit('new_selection'); } }, 1 => 0, 'Select within' => sub { my ($item, $self) = @_; $self->{selecting} = 'that_are_within'; }, 'Select containing' => sub { my ($item, $self) = @_; $self->{selecting} = 'that_contain'; }, 'Select intersecting' => sub { my ($item, $self) = @_; $self->{selecting} = 'that_intersect'; }, 1 => 0, 'Clear drawing' => sub { my ($item, $self) = @_; if ($self->{drawing}) { delete $self->{drawing}; $self->update_image; } }, 1 => 0, 'Set _background color..' => sub { my ($item, $self) = @_; my $color = $self->{bg_color}; my $d = Gtk2::ColorSelectionDialog->new('Color for the background'); my $c = new Gtk2::Gdk::Color ($color ? $color->[0]*257 : 0, $color ? $color->[1]*257 : 0, $color ? $color->[2]*257 : 0); $d->colorsel->set_current_color($c); if ($d->run eq 'ok') { $c = $d->colorsel->get_current_color; $d->destroy; $self->{bg_color} = [int($c->red/257),int($c->green/257),int($c->blue/257),255]; $self->render; } else { $d->destroy}; }, '_Export as PNG' => sub { my ($item, $self) = @_; my $filename = file_chooser('Export as a PNG image', 'save'); if ($filename) { my $type = 'png'; if (-e $filename) { my $dialog = Gtk2::MessageDialog->new(undef,'destroy-with-parent', 'question', 'yes_no', "Overwrite existing $filename?"); my $ret = $dialog->run; $filename = '' if $ret eq 'no'; $dialog->destroy; } $self->render(filename=>$filename, type=>$type) if $filename; } }, 'Res_tore' => sub { my ($item, $self) = @_; $self->update_image; }); my $item_setup = sub { my($item, $self) = @_; for ($item) { $_ .= ' x', last if /contain/ and $self->{selecting} =~ /contain/; $_ .= ' x', last if /within/ and $self->{selecting} =~ /within/; $_ .= ' x', last if /intersect/ and $self->{selecting} =~ /intersect/; $_ .= ' x', last if /_Zoom/ and $self->{rubberband_mode} =~ /zoom/; $_ .= ' x', last if /Pan/ and $self->{rubberband_mode} =~ /pan/; $_ .= ' x' if /_Select/ and $self->{rubberband_mode} =~ /select/; $_ .= ' x' if /Measure/ and $self->{rubberband_mode} =~ /measure/; $_ .= ' x' if /Draw/ and $self->{rubberband_mode} =~ /draw/; $_ .= ' x' if /Edit drawing/ and $self->{rubberband_mode} =~ /edit/; $_ .= ' x' if /Line/ and $self->{rubberband_geometry} =~ /line/; $_ .= ' x' if /Path/ and $self->{rubberband_geometry} =~ /path/; $_ .= ' x' if /Rect/ and $self->{rubberband_geometry} =~ /rect/; $_ .= ' x' if /Ellipse/ and $self->{rubberband_geometry} =~ /ellipse/; $_ .= ' x' if /Polygon/ and $self->{rubberband_geometry} =~ /polygon/; } return $item; }; return (\@menu, $item_setup); } ## @fn layer_menu() # @brief The callback for button_press_event in the layer list tree view. sub layer_menu { my($tree_view, $event, $self) = @_; my $layer; my @layers; my $selection = $tree_view->get_selection; my @rows = $selection->get_selected_rows; my @res = $tree_view->get_path_at_pos($event->x, $event->y); return unless defined $res[0]; my $index = $res[0] ? $res[0]->to_string : ''; my $column = $res[1] ? $res[1]->get_title : ''; my $path = Gtk2::TreePath->new($index); if (@rows < 2) { $layer = $self->{overlay}->get_layer_by_index($index); return unless $layer; } else { for my $r (@rows) { $layer = $self->{overlay}->get_layer_by_index($r->to_string); push @layers, $layer; } } if ($event->button == 3) { $tree_view->set_cursor($path); my $hide = $layer->visible() ? '_Hide' : '_Show'; my @items = @layers ? ( '_Hide' => sub { my($layers, $self) = @{$_[1]}; for my $layer (@$layers) { $layer->visible(0); } $self->update; $self->{overlay}->render; }, '_Show' => sub { my($layers, $self) = @{$_[1]}; for my $layer (@$layers) { $layer->visible(1); } $self->update; $self->{overlay}->render; }, '_Remove' => sub { my($layers, $self) = @{$_[1]}; for my $layer (@$layers) { $self->{model}->remove($layer->{_tree_index}); $self->{overlay}->remove_layer_by_name($layer->name); } $self->{overlay}->render; } ) : ( '_Zoom to' => sub { my($layer, $self) = @{$_[1]}; $self->{overlay}->zoom_to($layer); }, '_Up' => sub { my($layer, $self) = @{$_[1]}; $self->move_up(); }, '_Down' => sub { my($layer, $self) = @{$_[1]}; $self->move_down(); }, $hide => sub { my($layer, $self) = @{$_[1]}; $layer->visible(!$layer->visible()); $self->update; $self->{overlay}->render; }, '_Remove' => sub { my($layer, $self) = @{$_[1]}; $self->delete_selected(); } ); # add items from the layer classes unless (@layers) { push @items, ( 1 => 0 ); push @items, $layer->menu_items(); } my $menu = Gtk2::Menu->new; my $params = @layers ? [\@layers, $self] : [$layer, $self]; $i = 0; for (my $i =0; $i < @items; $i+=2) { my $item; unless ($items[$i+1]) { $item = Gtk2::SeparatorMenuItem->new(); } else { $item = Gtk2::MenuItem->new($items[$i]); $item->signal_connect(activate => $items[$i+1], $params); } $item->show; $menu->append($item); } $menu->popup(undef, undef, undef, undef, $event->button, $event->time); return 1; } elsif ($column =~ /^v/) { $layer->visible(!$layer->visible()); $self->update; $self->{overlay}->render; } elsif ($column =~ /^t/) { $tree_view->columns_autosize(); } return 0; } ## @ignore sub show_information { my($overlay, $self) = @_; my($x, $y) = $overlay->event_pixel2point; my $layer = $self->get_selected_layer(); my $location = sprintf("(x,y) = (%.4f, %.4f)", $x, $y); my $value = ''; if ($layer and $layer->isa('Geo::Raster')) { my @ij = $layer->w2g($x, $y); $location .= sprintf(", (i,j) = (%i, %i)",@ij); $value = $layer->point($x, $y); if (defined $value and $value ne 'nodata' and $layer->{INFO}) { $value = $layer->{TABLE}->{DATA}->[$value]->[$layer->{INFO}-1]; } } $self->{statusbar}->pop(0); $value = '' unless defined $value; # additional info, based on mode my($dim, $val) = $self->{overlay}->rubberband_value(); if (defined $dim) { $dim = $dim == 1 ? 'length' : 'area'; if (defined $val) { my $d = ''; if ($val > 1000000) { $val /= 1000000; $d = 'M'; } elsif ($val > 1000) { $val /= 1000; $d = 'k'; } $val = sprintf(" $dim = %.2f$d", $val); } else { $val = " $dim not computed"; } } else { $val = ''; } my $mode = $self->{overlay}->rubberband_mode(); $self->{statusbar}->push(0, "$mode $location $value$val"); } ## @ignore sub inspect { my($self, $data, $name) = @_; $name = 'unknown variable' unless $name; Gtk2::Ex::Geo::Layer::bootstrap_dialog( $self, $self, 'inspect_dialog', "Inspecting ".$name, { inspect_dialog => [delete_event => \&close_inspect, [$self]], inspect_close_button => [clicked => \&close_inspect, [$self]], } ); $data = \$data unless ref $data; $name =~ s/_/__/g; my $treedumper = Gtk2::Ex::Geo::TreeDumper->new ( data => $data, title => $name, dumper_setup => {} ); $treedumper->{tree_view}->modify_font(Gtk2::Pango::FontDescription->from_string('monospace')); $treedumper->{tree_view}->collapse_all; my $scroller = $self->{inspect_dialog}->get_widget('inspect_scrolledwindow'); $scroller->remove($self->{treedumper}->{tree_view}) if $self->{treedumper}; $self->{treedumper} = $treedumper; $scroller->add($treedumper->{tree_view}); $scroller->show_all; } ##@ignore sub close_inspect { my $self; for (@_) { next unless ref eq 'ARRAY'; ($self) = @{$_}; } Gtk2::Ex::Geo::Layer::hide_dialog($self, 'inspect_dialog'); 1; } ## @method set_layer # update the layer data in the layer list sub set_layer { my($self, $layer) = @_; my($type, $colors, $visible, $alpha); $type = ''; $alpha = $layer->alpha(); $alpha = 'Layer' if ref($alpha); $type = $layer->type; $visible = $layer->visible ? 'X' : ' '; $self->{model}->set ($layer->{_tree_index}, 0, $layer->name(), 1, $type, 2, $visible, 3, $alpha, ); } ## @method Gtk2::Ex::Geo::Layer add_layer($object, $name, $do_not_zoom_to) # @brief Add a layer to the overlay and the tree store # # The default behavior is to zoom to the new layer. The layer is # upgraded using the upgrade method of the registered layer classes. # # @param object A geospatial data object. Must be either an object of # a subclass of Gtk2::Ex::Geo::Layer or a data object that is # recognized by such. It is the responsibility of the upgrade method # of the layer class to upgrade the data object to a layer object. # @param name (optional) Name for the new layer. # @param do_not_zoom_to (optional) Whether to not to zoom the overlay # to this layer. Forwarded to Gtk2::Ex::Geo::Overlay::add_layer. # @return sub add_layer { my($self, $object, $name, $do_not_zoom_to) = @_; return unless $object; my $layer; for $upgrade (@{$self->{upgrades}}) { $layer = $upgrade->($object); last if $layer; } if ($layer) { $layer = $object if $layer == 1; # backwards compatibility } else { $layer = $object; } return unless $layer->isa('Gtk2::Ex::Geo::Layer'); my $i = $self->{overlay}->index_of_layer($name) if defined $name; croak "layer with name $name already exists" if defined $i; $layer->name($name); $layer->{_tree_index} = $self->{model}->insert (undef, 0); $self->set_layer($layer); $self->{overlay}->add_layer($layer, $do_not_zoom_to); return $layer; } ## @method Gtk2::Ex::Geo::Layer layer($name) # @param name # @return sub layer { my($self, $name) = @_; return $self->{overlay}->get_layer_by_name($name); } ## @method layers # @return a list of all layers (not the internal list but a copy) sub layers { my($self) = @_; my @a = @{$self->{overlay}->{layers}}; return @a; } ## @method get_focal($name) # @brief Returns a selected (or visible) part of a raster layer by its name. # @deprecated Selected and clip are implemented elsewhere. sub get_focal { my($self, $name) = @_; my $gd = $self->{overlay}->get_layer_by_name($name); if ($gd and $gd->isa('Geo::Raster')) { my @clip = $self->{overlay}->get_focus; @clip = $gd->wa2ga(@clip); # do not expand the view $clip[2]--; $clip[3]--; return $gd->clip(@clip); } } ## @method update # @brief Updates the whole layer list. sub update { my($self) = @_; for my $layer (@{$self->{overlay}->{layers}}) { $self->set_layer($layer); } } ## @ignore sub swap { my($array,$i1,$i2) = @_; my $e1 = $array->[$i1]; my $e2 = $array->[$i2]; $array->[$i1] = $e2; $array->[$i2] = $e1; return ($e1,$e2); } ## @method move_down # @brief Moves the selected layer down in the list. sub move_down { my($self) = @_; my ($path, $focus_column) = $self->{tree_view}->get_cursor; return unless $path; my $index = $path->to_string; my $n = $#{$self->{overlay}->{layers}}; if ($index < $n) { my($layer1,$layer2) = swap($self->{overlay}->{layers},$n-$index,$n-$index-1); $self->{model}->move_after($layer1->{_tree_index},$layer2->{_tree_index}); $self->{overlay}->render; } } ## @method move_up # @brief Moves the selected layer up in the list. sub move_up { my($self) = @_; my ($path, $focus_column) = $self->{tree_view}->get_cursor; return unless $path; my $index = $path->to_string; my $n = $#{$self->{overlay}->{layers}}; if ($index > 0) { my($layer1,$layer2) = swap($self->{overlay}->{layers},$n-$index,$n-$index+1); $self->{model}->move_before($layer1->{_tree_index},$layer2->{_tree_index}); $self->{overlay}->render; } } ## @method remove_layer # @brief Removes the selected layer. # @return Success or failure sub remove_layer { my($self, $name, $do_not_render) = @_; my $layer = $self->{overlay}->get_layer_by_name($name); return unless $layer; $self->{model}->remove($layer->{_tree_index}); $self->{overlay}->remove_layer_by_name($name); delete($self->{focused}) if $self->{focused} and $self->{focused}->name eq $layer->name; $layer->close($self); $self->{overlay}->render unless $do_not_render; return 1; } ## @method delete_selected # @brief Removes the selected layer and destroys it. # @return Success or failure sub delete_selected { my($self, $do_not_render) = @_; my $n = $#{$self->{overlay}->{layers}}; return if $n < 0; my ($path, $focus_column) = $self->{tree_view}->get_cursor; return unless $path; my $index = $path->to_string; return if $index < 0 or $index > $n; my($layer) = splice(@{$self->{overlay}->{layers}}, $n-$index, 1); $self->{model}->remove($layer->{_tree_index}); delete($self->{focused}) if $self->{focused} and $self->{focused}->name eq $layer->name; $layer->close($self); if ($n > 0) { $index-- if $index == $n; $self->{tree_view}->set_cursor(Gtk2::TreePath->new($index)); } $self->{overlay}->render unless $do_not_render; return 1; } ## @method get_selected # @brief Returns the selected layer. sub get_selected_layer { my($self) = @_; my($path, $focus_column) = $self->{tree_view}->get_cursor; return unless $path; my $index = $path->to_string; return $self->{overlay}->get_layer_by_index($index); } ## @method select_layer($name) # @brief Selects a layer. sub select_layer { my($self, $name) = @_; my $index = $self->{overlay}->index_of_layer($name); if (defined $index) { $self->{tree_view}->set_cursor(Gtk2::TreePath->new($index)); } } ## @ignore # explain this in some dox file sub eval_entry { my($entry, $event, $self) = @_; my $key = $event->keyval; my $text = $entry->get_text; $self->{history}->editing($text); if ($text ne '' and $key == $Gtk2::Gdk::Keysyms{Return}) { $self->{history}->enter(); $entry->set_text(''); my $focal = 0; # default is global if ($text =~ /^focal:\s*/) { $text =~ s/^focal:\s*//; $focal = 1; } for my $sub ('inspect') { $text =~ s/^$sub/\$self\-\>$sub/ if $text =~ /^$sub\(/; } my($function) = $text =~ /^(\w+)\b/; if ($function and $self->{functions}{$function}) { if ($self->{functions}{$function}{sub}) { $text =~ s/^$function/$self->{functions}{$function}{sub}/; } else { # object $text =~ s/^$function/\$self->{functions}{$function}{object}-\>$function/; } } my @g = $text =~ /\$(\w+)/g; my @_gd; for my $i (0..$#g) { $_gd[$i] = $focal ? $self->get_focal($g[$i]) : $self->{overlay}->get_layer_by_name($g[$i]); next unless $_gd[$i]; $text =~ s/\$$g[$i]\b/\$_gd[$i]/; } { no strict; eval $text; croak "$text\n$@" if $@; } for my $i (0..$#g) { if ($self->{overlay}->get_layer_by_name($g[$i])) { $_gd[$i]->value_range() if $_gd[$i]->isa('Geo::Raster'); } else { eval "\$self->add_layer(\$$g[$i],'$g[$i]',1);" if $g[$i] and $g[$i] ne 'self'; } } undef @_gd; $self->update(); $self->{overlay}->render; return 1; } elsif ($key == $Gtk2::Gdk::Keysyms{Up}) { $entry->set_text($self->{history}->arrow_up); return 1; } elsif ($key == $Gtk2::Gdk::Keysyms{Down}) { $entry->set_text($self->{history}->arrow_down); return 1; } } sub render { $_[0]->{overlay}->render; } sub simulate { my($self, $sub) = @_; $self->stop; $self->{_event_source} = Glib::Idle->add($sub); } sub stop { my($self) = @_; if ($self->{_event_source}) { Glib::Source->remove($self->{_event_source}); delete $self->{_event_source}; } } sub min { $_[0] > $_[1] ? $_[1] : $_[0]; } sub max { $_[0] > $_[1] ? $_[0] : $_[1]; } ## @class Gtk2::Ex::Geo::History # @brief Input history a'la (at least attempting) GNU history package Gtk2::Ex::Geo::History; sub new { my ($class, $history) = @_; push @$history,'' unless defined($history->[$#$history]) and $history->[$#$history] eq ''; my $self = { index => $#$history, history => $history, edit_index => -1, edit_text => '', }; bless($self, $class); } sub arrow_up { my $self = shift; if ($self->{edit_index} >= 0) { $self->{history}->[$self->{edit_index}] = $self->{edit_text}; } $self->{index} = max(0, $self->{index}-1); return $self->{edit_text} if $self->{edit_index} == $self->{index}; return $self->{history}->[$self->{index}]; } sub arrow_down { my $self = shift; if ($self->{edit_index} >= 0) { $self->{history}->[$self->{edit_index}] = $self->{edit_text}; } $self->{index} = min($#{$self->{history}}, $self->{index}+1); return $self->{edit_text} if $self->{edit_index} == $self->{index}; return $self->{history}->[$self->{index}]; } sub editing { my ($self, $text) = @_; $self->{edit_index} = $self->{index}; $self->{edit_text} = $text; } sub enter { my ($self, $text) = @_; $self->{edit_text} = $text if defined $text; my $history = $self->{history}; if ($#$history >= 0) { unless ($#$history > 0 and $history->[$#$history-1] eq $self->{edit_text}) { $history->[$#$history] = $self->{edit_text}; push @$history,'' unless $self->{edit_text} eq ''; } $self->{index} = $#$history; $self->{edit_index} = -1; $self->{edit_text} = ''; } } sub min { $_[0] > $_[1] ? $_[1] : $_[0]; } sub max { $_[0] > $_[1] ? $_[0] : $_[1]; } 1; __DATA__