## @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] =~ /^\; # remove the extra content
shift @buffer if $buffer[0] =~ /^\s*$/;
register_dialogs($self, Gtk2::Ex::Geo::DialogMaster->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__