## @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 UNIVERSAL qw(isa); use Carp; use Gtk2::Ex::Geo::Overlay; use Gtk2::Ex::Geo::Layer; use Gtk2::Ex::Geo::Dialogs; 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 = {}; my @columns = qw /name type ? a/; $self->{model} = Gtk2::TreeStore->new(qw/Glib::String Glib::String Glib::String Glib::String/); $self->{tree_view} = Gtk2::TreeView->new($self->{model}); my $selection = $self->{tree_view}->get_selection; $selection->set_mode('multiple'); $self->{toolbar} = Gtk2::Toolbar->new(); $self->{statusbar} = Gtk2::Statusbar->new(); $self->{entry} = Gtk2::Entry->new(); $self->{overlay} = Gtk2::Ex::Geo::Overlay->new(); my $i = 0; foreach my $column (@columns) { my $cell = Gtk2::CellRendererText->new; my $col = Gtk2::TreeViewColumn->new_with_attributes($column, $cell, text => $i++); $self->{tree_view}->append_column($col); } my($menu, $menu_item_setup) = overlay_menu(); $self->{overlay}->my_inits( menu => $menu, menu_item_setup => $menu_item_setup, rubberband_mode => 'zoom', rubberband_geometry => 'rect', selecting => 'that_are_within', ); $self->{tree_view}->signal_connect ( cursor_changed => sub { my(undef, $gis) = @_; $gis->get_selected_layer(); }, $self); $self->{overlay}->set_draw_on ( sub { my($gis, $pixmap) = @_; my $overlay = $gis->{overlay}; my $layer = $gis->get_selected_layer(); if ($layer) { my $gc = Gtk2::Gdk::GC->new($pixmap); $gc->set_rgb_fg_color(Gtk2::Gdk::Color->new(65535,0,0)); $layer->render_selection($gc, $overlay); } }, $self); $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) if $layer->dialog_visible('features_dialog'); } }, $self); if ($params{history}) { if (open TMP, $params{history}) { my @history = ; CORE::close TMP; for (@history) { chomp $_; s/\r//; } $self->{history} = new Gtk2::Ex::Geo::History(\@history); } else { carp("$!: $params{history} (it will be created at exit)"); } $self->{history_file} = $params{history}; } $self->{history} = new Gtk2::Ex::Geo::History(['']) unless $self->{history}; if ($params{resources}) { if (open TMP, $params{resources}) { my $key = ''; while () { chomp $_; s/\r//; if (/^ /) { s/^ //; $self->{resources}{$key}{$_} = 1; } else { $key = $_; } } CORE::close TMP; } else { carp("$!: $params{resources} (it will be created at exit)"); } $self->{resources_file} = $params{resources}; } my @buffer = ; pop @buffer unless $buffer[$#buffer] =~ /^\new(buffer => \@buffer)); $self->{tree_view}->signal_connect(button_press_event => \&layer_menu, $self); $self->{entry}->signal_connect(key_press_event => \&eval_entry, $self); $self->{overlay}->signal_connect(motion_notify => \&show_information, $self); bless($self, $class); } ## @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 isa($dialogs, 'Gtk2::Ex::Geo::DialogMaster'); push @{$self->{dialogs}}, $dialogs; } ## @method register_commands($commands) # @brief Extend the capabilities by adding new commands sub register_commands { my($self, $commands) = @_; for (sort {$commands->{$a}{nr} <=> $commands->{$b}{nr}} keys %$commands) { my $button = Gtk2::ToolButton->new(undef, $commands->{$_}{text}); my $tooltips = Gtk2::Tooltips->new; my $tip = $commands->{$_}{tip} || ''; $button->set_tooltip($tooltips, $tip, ''); $tooltips->set_tip($button, $tip); $tooltips->enable; $self->{toolbar}->insert($button, $commands->{$_}{pos}); $button->signal_connect('clicked', $commands->{$_}{sub}, $self); $self->{commands}{$_} = $commands->{$_}{sub}; $button->show; } } 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($params{class}); %params = %$registration; } $self->register_dialogs($params{dialogs}) if $params{dialogs}; $self->register_commands($params{commands}) if $params{commands}; } ## @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\n"; } } close TMP; } else { croak "$!: $self->{resources_file}"; } } for my $layer (@{$self->{overlay}->{layers}}) { $layer->destroy_dialogs; } delete $self->{dialogs}; $self->{overlay}->close; for ('inspect_dialog', 'statusbar', 'toolbar', 'entry', 'tree_view', 'overlay') { #$self->{$_}->destroy if $self->{$_}; there is no GladeXML->destroy delete $self->{$_}; } } ## @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 $parent = $self->{main_window} if $self->{main_window}; 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 _in' => { nr => 1, sub => sub { my ($item, $self) = @_; $self->zoom_in(); }, }, 'Zoom _out' => { nr => 2, sub => sub { my ($item, $self) = @_; $self->zoom_out(); }, }, 'Zoom to pre_vious' => { nr => 3, sub => sub { my ($item, $self) = @_; my $zoom = pop @{$self->{zoom_stack}}; $self->zoom(@$zoom, 0, 1) if $zoom; }, }, 4 => { nr => 4, }, '_Zoom' => { nr => 5, sub => sub { my ($item, $self) = @_; $self->{rubberband_mode} = 'zoom'; }, }, '_Pan' => { nr => 6, sub => sub { my ($item, $self) = @_; $self->{rubberband_mode} = 'pan'; }, }, '_Select' => { nr => 7, sub => sub { my ($item, $self) = @_; $self->{rubberband_mode} = 'select'; }, }, '_Measure' => { nr => 8, sub => sub { my ($item, $self) = @_; $self->{rubberband_mode} = 'measure'; }, }, 9 => { nr => 9, }, '_Line' => { nr => 10, sub => sub { my ($item, $self) = @_; $self->{rubberband_geometry} = 'line'; }, }, 'Path' => { nr => 11, sub => sub { my ($item, $self) = @_; $self->{rubberband_geometry} = 'path'; }, }, '_Rectangle' => { nr => 12, sub => sub { my ($item, $self) = @_; $self->{rubberband_geometry} = 'rect'; }, }, '_Ellipse' => { nr => 13, sub => sub { my ($item, $self) = @_; $self->{rubberband_mode} = 'measure'; $self->{rubberband_geometry} = 'ellipse'; }, }, 'Polygon' => { nr => 14, sub => sub { my ($item, $self) = @_; $self->{rubberband_geometry} = 'polygon'; }, }, 15 => { nr => 15, }, 'Reselect' => { nr => 16, sub => sub { my ($item, $self) = @_; $self->signal_emit('new_selection'); } }, '_Clear selection' => { nr => 17, sub => sub { my ($item, $self) = @_; if ($self->{selection}) { delete $self->{selection}; $self->signal_emit('new_selection'); } }, }, 18 => { nr => 18, }, 'Select within' => { nr => 19, sub => sub { my ($item, $self) = @_; $self->{selecting} = 'that_are_within'; }, }, 'Select containing' => { nr => 20, sub => sub { my ($item, $self) = @_; $self->{selecting} = 'that_contain'; }, }, 'Select intersecting' => { nr => 21, sub => sub { my ($item, $self) = @_; $self->{selecting} = 'that_intersect'; }, }, 21 => { nr => 22, }, 'Set _background color..' => { nr => 23, sub => 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)]; $self->render; } else { $d->destroy}; }, }, '_Export as PNG' => { nr => 24, sub => sub { my ($item, $self) = @_; my $filename; my $type = 'png'; my $file_chooser = Gtk2::FileChooserDialog->new ('Export as a PNG image', undef, 'save', 'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok'); my $folder = $file_chooser->get_current_folder; $file_chooser->set_current_folder($self->{folder}) if $self->{folder}; if ('ok' eq $file_chooser->run) { # you can get the user's selection as a filename or a uri. $self->{folder} = $file_chooser->get_current_folder; $filename = $file_chooser->get_filename; 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; } } $file_chooser->set_current_folder($folder); $file_chooser->destroy; $self->render(filename=>$filename, type=>$type) if $filename; }, }, 'Res_tore' => { nr => 25, sub => 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 /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($tv, $event, $self) = @_; my $layer; my @layers; my $column; my $selection = $self->{tree_view}->get_selection; my @rows = $selection->get_selected_rows; if (@rows < 2) { my @res = $self->{tree_view}->get_path_at_pos($event->x, $event->y); return unless defined $res[0]; $self->{tree_view}->set_cursor(Gtk2::TreePath->new($res[0]->to_string)); $layer = $self->{overlay}->get_layer_by_index($res[0]->to_string); return unless $layer; $column = $res[1]->get_title; } else { for my $r (@rows) { $layer = $self->{overlay}->get_layer_by_index($r->to_string); push @layers, $layer; } $column = ''; } if ($event->button == 3) { my $hide = $layer->visible() ? '_Hide' : '_Show'; my %items = @layers ? ( '_Hide' => { nr => 4, sub => sub { my($layers, $self) = @{$_[1]}; for my $layer (@$layers) { $layer->visible(0); } $self->update; $self->{overlay}->render; } }, '_Show' => { nr => 4, sub => sub { my($layers, $self) = @{$_[1]}; for my $layer (@$layers) { $layer->visible(1); } $self->update; $self->{overlay}->render; } }, '_Remove' => { nr => 5, sub => 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' => { nr => 1, sub => sub { my($layer, $self) = @{$_[1]}; $self->{overlay}->zoom_to($layer); } }, '_Up' => { nr => 2, sub => sub { my($layer, $self) = @{$_[1]}; $self->move_up(); } }, '_Down' => { nr => 3, sub => sub { my($layer, $self) = @{$_[1]}; $self->move_down(); } }, $hide => { nr => 4, sub => sub { my($layer, $self) = @{$_[1]}; $layer->visible(!$layer->visible()); $self->update; $self->{overlay}->render; } }, '_Remove' => { nr => 5, sub => sub { my($layer, $self) = @{$_[1]}; $self->delete_selected(); } } ); # add items from the layer classes $layer->menu_items(\%items) unless @layers; my $menu = Gtk2::Menu->new; my $params = @layers ? [\@layers, $self] : [$layer, $self]; $i = 0; for $cmd (sort {$items{$a}{nr} <=> $items{$b}{nr}} keys %items ) { my $item; unless ($items{$cmd}{sub}) { $item = Gtk2::SeparatorMenuItem->new(); } else { $item = Gtk2::MenuItem->new($cmd); $item->signal_connect(activate => $items{$cmd}{sub}, $params); } $item->show; $menu->append($item); } $menu->popup(undef, undef, undef, undef, $event->button, $event->time); return 1; } elsif ($column eq '?') { $layer->visible(!$layer->visible()); $self->update; $self->{overlay}->render; } 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 ref($layer) =~ /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 = ''; } $self->{statusbar}->push(0, "$self->{overlay}->{rubberband_mode} $location $value$val"); } ## @ignore sub inspect { my($self, $data, $name) = @_; my $dialog = $self->{inspect_dialog}; unless ($dialog) { $self->{inspect_dialog} = $dialog = $self->get_dialog('inspect_dialog'); croak "inspect_dialog for Gtk2::Ex::Geo::Glue does not exist" unless $dialog; $dialog->get_widget('inspect_dialog')->signal_connect(delete_event => \&close_inspect, [$self]); $dialog->get_widget('inspect_close_button')->signal_connect(clicked => \&close_inspect, [$self]); } elsif (!$dialog->get_widget('inspect_dialog')->get('visible')) { $dialog->get_widget('inspect_dialog')->move(@{$self->{inspect_dialog_position}}); } $name = 'unknown variable' unless $name; $dialog->get_widget('inspect_dialog')->set_title("Inspecting ".$name); $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 = $dialog->get_widget('inspect_scrolledwindow'); $scroller->remove($self->{treedumper}->{tree_view}) if $self->{treedumper}; $self->{treedumper} = $treedumper; $scroller->add($treedumper->{tree_view}); $dialog->get_widget('inspect_dialog')->show_all; $dialog->get_widget('inspect_dialog')->present; } ##@ignore sub close_inspect { my $self; for (@_) { next unless ref eq 'ARRAY'; ($self) = @{$_}; } my $dialog = $self->{inspect_dialog}->get_widget('inspect_dialog'); $self->{inspect_dialog_position} = [$dialog->get_position]; $dialog->hide(); 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($layer, $name, $do_not_zoom_to) # @brief adds $layer with $name to overlay and model # The default behavior is to zoom to the new layer. # Geo::Raster is upgraded to Geo::Raster::Layer # Geo::Vector is upgraded to Geo::Vector::Layer # @param layer a Gtk2::Ex::Geo::Layer object # @param name [optional] a name for the layer # @param do_not_zoom_to [optional] the overlay is zoomed to the added # layer unless this is defined # @return sub add_layer { my($self, $layer, $name, $do_not_zoom_to) = @_; return unless $layer; for $upgrade (@{$self->{upgrades}}) { $layer = $upgrade->($layer); } croak "can add only Gtk2::Ex::Geo::Layer objects" unless isa($layer, '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; $name = $layer->name unless defined $name; $layer->defaults(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 ref($gd) =~ /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. # @deprecated Use delete_selected sub remove_layer { my($self, $name) = @_; 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); $self->{overlay}->render; } ## @method delete_selected # @brief Removes the selected layer and destroys it. sub delete_selected { 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 and $index <= $n) { my($layer) = splice(@{$self->{overlay}->{layers}},$n-$index,1); $self->{model}->remove($layer->{_tree_index}); if ($n > 0) { $index-- if $index == $n; $self->{tree_view}->set_cursor(Gtk2::TreePath->new($index)); } $layer->destroy_dialogs; $self->{overlay}->render; } } ## @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 ref($_gd[$i]) =~ /Raster/; } else { eval "\$self->add_layer(\$$g[$i],'$g[$i]',1) if ". "isa(\$$g[$i], 'Gtk2::Ex::Geo::Layer') or isa(\$$g[$i], 'Geo::Raster') or isa(\$$g[$i], 'Geo::Vector');" 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 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__