Geo-OGC-Service-WFS
view release on metacpan or search on metacpan
lib/Geo/OGC/Service/WFS.pm view on Meta::CPAN
$writer->close_element;
}
=pod
=head3 DescribeFeatureType
Service the DescribeFeatureType request.
=cut
sub DescribeFeatureType {
my ($self) = @_;
my @typenames;
for my $query (@{$self->{request}{queries}}) {
push @typenames, split(/\s*,\s*/, $query->{typename});
}
unless (@typenames) {
$self->error({ exceptionCode => 'MissingParameterValue',
locator => 'typeName' },
[$self->CORS]);
return;
}
my %types;
for my $name (@typenames) {
eval {
$types{$name} = $self->get_feature_type($name);
};
unless ($types{$name}) {
$@ =~ s/ at \/.*//;
$self->error({ exceptionCode => 'InvalidParameterValue',
locator => 'typeName',
ExceptionText => $@ },
[$self->CORS]);
return;
}
}
my $writer = Geo::OGC::Service::XMLWriter::Caching->new();
$writer->open_element(schema => {
targetNamespace => $self->{config}{targetNamespace},
xmlns => "http://www.w3.org/2001/XMLSchema",
'xmlns:gml' => "http://schemas.opengis.net/gml",
elementFormDefault => "qualified",
attributeFormDefault => "unqualified" });
$writer->element(import => {
namespace => "http://www.opengis.net/gml",
schemaLocation => "http://schemas.opengis.net/gml/2.1.2/feature.xsd" });
for my $name (sort keys %types) {
my $type = $types{$name};
# to do: geometry and primary key
my ($pseudo_credentials) = pseudo_credentials($type);
my @elements;
for my $property (keys %{$type->{columns}}) {
next if $pseudo_credentials->{$property};
my $minOccurs = 0;
push @elements, ['element', {
name => $type->{columns}{$property}{out_name},
type => $type->{columns}{$property}{out_type},
minOccurs => "$minOccurs",
maxOccurs => "1"
}];
}
$writer->element(
'complexType',
{name => $type->{Name}.'Type'},
['complexContent',
['extension', { base => 'gml:AbstractFeatureType' },
['sequence', \@elements
]]]
);
$writer->element(
'element', { name => $type->{Name},
type => 'ogr:'.$type->{Name}.'Type',
substitutionGroup => 'gml:_Feature' } );
}
$writer->close_element();
$writer->stream($self->{responder});
}
=pod
=head3 GetPropertyValue
Not yet implemented.
=cut
sub GetPropertyValue {
}
=pod
=head3 GetFeature
Service the GetFeature request. The response is generated with the GML
driver of GDAL using options TARGET_NAMESPACE, PREFIX, and
FORMAT. They are from the root of the configuration (TARGET_NAMESPACE
from FeatureType falling back to root) falling back to default ones
"http://www.opengis.net/wfs", "wfs", and "GML3.2". The content type of
the reponse is from configuration (root key 'Content-Type') falling
back to default "text/xml".
The "gml:id" attribute of the features is GDAL generated or from a
field defined by the key "gml:id" in the configuration (in the hash
with layer name falling back to FeatureType).
=cut
sub GetFeature {
my ($self) = @_;
my $query = $self->{request}{queries}[0]; # actually we should loop through all queries?
my ($typename) = split(/\s*,\s*/, $query->{typename});
unless ($typename) {
$self->error({ exceptionCode => 'MissingParameterValue',
locator => 'typeName' },
[$self->CORS]);
return;
}
my $type;
eval {
$type = $self->get_feature_type($typename);
};
unless ($type) {
$@ =~ s/ at \/.*//;
$self->error({ exceptionCode => 'InvalidParameterValue',
locator => 'typeName',
ExceptionText => $@ },
[$self->CORS]);
return;
}
my $ds = Geo::OGR::Open($type->{DataSource});
my $layer;
my $epsg = $query->{EPSG} // $type->{SRID};
my @bbox;
my $bbox_crs;
if ($query->{BBOX}) {
@bbox = @{$query->{BBOX}};
$bbox_crs = 4326; # WFS 1.1.0 14.3.3
if (@bbox == 5) {
$bbox_crs = epsg_number(pop @bbox);
}
}
my $filter = filter2sql($query->{filter}, $type) // '';
# pseudo_credentials: these fields are required to be in the filter and they are not included as attributes
my ($pseudo_credentials, @pseudo_credentials) = pseudo_credentials($type);
if (@pseudo_credentials) {
# test for pseudo credentials in filter
my $pat1 = "\\(\\(\"$pseudo_credentials[0]\" = '.+?'\\) AND \\(\"$pseudo_credentials[1]\" = '.+?'\\)\\)";
my $pat2 = "\\(\\(\"$pseudo_credentials[1]\" = '.+?'\\) AND \\(\"$pseudo_credentials[0]\" = '.+?'\\)\\)";
my $n = join(' and ', @pseudo_credentials);
unless ($filter and ($filter =~ /$pat1/ or $filter =~ /$pat2/)) {
$self->error({ exceptionCode => 'InvalidParameterValue',
locator => 'filter',
ExceptionText => "Not authorized. Please provide '$n' in filter." });
return;
}
}
my @columns = ("$type->{'gml:id'} as gml_id");
# reverse the field names
for my $column (keys %{$type->{columns}}) {
next if $pseudo_credentials->{$column};
my $name = $type->{columns}{$column}{out_name};
next if $query->{properties} && not $query->{properties}{$name};
$filter =~ s/$name/$column/g if $filter;
push @columns, "\"$column\" as \"$name\"";
}
my $Geometry = $type->{GeometryColumn};
push @columns, "ST_Transform($Geometry,$epsg) as geometryProperty";
my $sql = "SELECT ".join(',',@columns)." FROM $type->{Table} WHERE ST_IsValid($Geometry)";
if ($filter) {
$filter =~ s/GeometryColumn/$Geometry/g;
$sql .= " AND $filter";
}
if (@bbox) {
my $bbox = join(",", @bbox);
$sql .= " AND (ST_Transform($Geometry,$epsg) && ST_Transform(ST_MakeEnvelope($bbox,$bbox_crs),$epsg))";
}
print STDERR "$sql\n" if $self->{debug};
eval {
$layer = $ds->ExecuteSQL($sql);
};
unless ($layer) {
say STDERR $sql,"\n",$@;
$self->error({ exceptionCode => 'Error',
ExceptionText => 'Internal error' });
return;
}
my $i = 0;
my $count = min($query->{count}, $self->{config}{max_count}, 1000000);
my $result_type = $query->{resulttype} // 'results'; # real data instead of number of features
my ($content_type, $driver);
# for GML creation options see http://www.gdal.org/drv_gml.html
# for GeoJSON creation options see http://www.gdal.org/drv_geojson.html
# the FORMAT, TARGET_NAMESPACE, and PREFIX need to be set for OpenLayers
# the following has worked
# OpenLayers 2: FORMAT (not set), TARGET_NAMESPACE (http://ogr.maptools.org/), and PREFIX (ogr)
# OpenLayers 4: FORMAT (GML3), TARGET_NAMESPACE (http://www.opengis.net/gml), and PREFIX (ogr)
# tweaked GML is for getting transactions to work with OpenLayers v4
my %creation_options;
$creation_options{FORMAT} = $self->{request}{outputformat} // $self->{config}{FORMAT} // $type->{FORMAT} // 'GML2';
for my $format ($creation_options{FORMAT}) {
# convert some known values to those understood by GDAL
$format = $OutputFormats{$format} if $format && $OutputFormats{$format};
if ($format =~ /GML/) {
for my $key (@GDAL_GML_Creation_options) {
next if $key eq 'FORMAT';
next unless exists $type->{$key} || exists $self->{config}{$key};
$creation_options{$key} = $type->{$key} // $self->{config}{$key};
}
$content_type = $self->{config}{'Content-Type'} // 'text/xml; charset=utf-8';
$driver = Geo::OGR::Driver('GML');
lib/Geo/OGC/Service/WFS.pm view on Meta::CPAN
}
}
unless ($pk_exists) {
if ($self->{debug} > 3) {
say STDERR "$table->[3]: 'gml:id ($pk[0]) does not exist in the table";
}
%geometries = ();
}
} elsif ($self->{debug} > 3) {
say STDERR "$table->[3] does not have single PK, maybe use gml:id in config?";
}
for my $geom (keys %geometries) {
my $ok;
my $sql = "select auth_name,auth_srid from $table->[2] ".
"join spatial_ref_sys on spatial_ref_sys.srid=st_srid(\"$geom\") ".
"limit 1";
$sth = $dbh->prepare($sql);
if ($sth) {
my $rv = $sth->execute;
if ($rv) {
my ($auth_name, $auth_srid) = $sth->fetchrow_array;
$auth_name //= 'undef';
$auth_srid //= 'undef';
$geometries{$geom} = [$auth_name, $auth_srid];
if ($auth_name eq 'EPSG' && $auth_srid) {
$ok = 1;
} elsif ($self->{debug} > 3) {
say STDERR "$table->[3]: auth of $geom is $auth_name:$auth_srid (is it empty?)";
}
} elsif ($self->{debug} > 3) {
say STDERR "$table->[3]: no permission";
}
}
delete $geometries{$geom} unless $ok;
}
unless (%geometries) {
if ($self->{debug} > 2) {
say STDERR "$table->[3]: is not readable with single integer pk and geometry column(s) with EPSG SRS";
}
next;
}
for my $geom (keys %geometries) {
my $name = $table->[3].'.'.$geom;
$name =~ s/ /_/g; # XML layer names can't have spaces
my $feature_type = {
Name => $name,
Title => $name,
Abstract => "Layer from $table->[1] in $table->[0] using column $geom.",
DefaultSRS => $geometries{$geom}[0].":".$geometries{$geom}[1],
SRID => $geometries{$geom}[1],
DataSource => $ds,
Table => $table->[2], # full quoted name with schema
GeometryColumn => '"'.$geom.'"',
columns => \%columns,
'gml:id' => $pk[0],
Operations => $type->{Operations}, # $self->{config}{Operations} is the default
require_user => $type->{require_user} // $self->{config}{require_user}, # operation dependent?
pseudo_credentials => $type->{pseudo_credentials},
# to do: these in ows:WGS84BoundingBox, GetCapabilities
#LowerCorner
#UpperCorner
};
for my $o (@GDAL_GML_Creation_options) {
$feature_type->{$o} = $type->{$o} if exists $type->{$o};
}
my ($h, @c) = pseudo_credentials($feature_type);
my $ok = 1;
for my $c (@c) {
unless ($feature_type->{columns}{$c}) {
carp "pseudo credential column '$c' not in table.\n";
$ok = 0;
next;
}
}
next unless $ok;
push @types, $feature_type;
}
}
#print STDERR Dumper \@types;
return \@types;
}
# return WFS request in a hash
# this function is written according to WFS 1.1.0 / 2.0.0
sub ogc_request {
my ($node) = @_;
my ($ns, $name) = parse_tag($node);
if ($name eq 'GetCapabilities') {
return { service => $node->getAttribute('service'),
request => $name,
version => $node->getAttribute('version') };
} elsif ($name eq 'DescribeFeatureType') {
my $request = { service => $node->getAttribute('service'),
request => $name,
version => $node->getAttribute('version') };
$request->{queries} = [];
for ($node = $node->firstChild; $node; $node = $node->nextSibling) {
my ($ns, $name) = parse_tag($node);
if ($name eq 'TypeName') {
push @{$request->{queries}}, { typename => $node->textContent };
}
}
return $request;
} elsif ($name eq 'GetFeature') {
my $request = { request => 'GetFeature' };
for my $a (qw/service version resultType outputFormat count maxFeatures/) {
my %map = (maxFeatures => 'count');
my $key = $map{$a} // $a;
my $b = $node->getAttribute($a);
$request->{lc($key)} = $b if $b;
}
$request->{queries} = [];
for ($node = $node->firstChild; $node; $node = $node->nextSibling) {
push @{$request->{queries}}, ogc_request($node);
}
return $request;
} elsif ($name eq 'Transaction') {
my $request = { request => 'Transaction' };
for my $a (qw/service version/) {
my $b = $node->getAttribute($a);
$request->{$a} = $b if $b;
}
for ($node = $node->firstChild; $node; $node = $node->nextSibling) {
lib/Geo/OGC/Service/WFS.pm view on Meta::CPAN
}
unless ($col) {
say STDERR "Can't find column name for property '$name'.";
next;
}
$set .= "$col = $val, ";
}
$set =~ s/, $//;
my $where = $get_filter->($node, $type);
say STDERR "Update: SET $set WHERE $where" if $self->{debug} > 1;
push @{$dbisql{$dbi}{Update}{SQL}}, "UPDATE $type->{Table} SET $set WHERE $where";
push @{$dbisql{$dbi}{Update}{type}}, $type;
}
for my $node (@{$self->{request}{Delete}}) {
my ($type_name, $type, $dbi) = $get_type->($node->getAttribute('typeName'));
next unless $type_name;
my $where = $get_filter->($node, $type);
say STDERR "Delete: $where" if $self->{debug} > 1;
push @{$dbisql{$dbi}{Delete}{SQL}}, "DELETE FROM $type->{Table} WHERE $where";
push @{$dbisql{$dbi}{Delete}{type}}, $type;
}
return \%dbisql;
}
# determine the EPSG number
sub epsg_number {
my $str = shift;
return undef unless $str;
my ($epsg) = $str =~ /^EPSG:(\d+)$/;
return $epsg if $epsg;
}
sub DataSource2dbi {
my $DataSource = shift;
my $dbi = $DataSource;
$dbi =~ s/^pg/Pg/i;
$dbi =~ s/ host/;host/;
$dbi =~ s/ port/;port/;
$dbi =~ s/ options/;options/;
$dbi =~ s/user=//;
$dbi =~ s/password=//;
return 'dbi:'.$dbi;
}
sub split_to_listref {
my $s = shift;
return undef unless $s;
return [split /\s*,\s*/, $s];
}
sub list2element {
my ($tag, $list) = @_;
my @element;
my @t = split /\s*,\s*/, $list;
for my $t (@t) {
push @element, [$tag, $t];
}
return @element;
}
sub pseudo_credentials {
my $type = shift;
my $c = $type->{pseudo_credentials};
return ({}) unless $c;
my($c1,$c2) = $c =~ /(\w+),(\w+)/;
return ({$c1 => 1,$c2 => 1},$c1,$c2);
}
sub min {
my $retval = shift;
for my $x (@_) {
next unless defined $x;
$retval = $x unless defined $retval;
$retval = $x if $x < $retval;
}
return $retval;
}
1;
__END__
=head1 SEE ALSO
Discuss this module on the Geo-perl email list.
L<https://list.hut.fi/mailman/listinfo/geo-perl>
For the WFS standard see
L<http://www.opengeospatial.org/standards/wfs>
=head1 REPOSITORY
L<https://github.com/ajolma/Geo-OGC-Service-WFS>
=head1 AUTHOR
Ari Jolma, E<lt>ari.jolma at gmail.comE<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2015- by Ari Jolma
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.22.0 or,
at your option, any later version of Perl 5 you may have available.
=cut
( run in 0.543 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )