view pkg/wfs/capabilities.go @ 5415:4ad68ab239b7 marking-single-beam

Factored creation of default class breaks in SR import to be reused with markings, too.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Wed, 07 Jul 2021 12:01:28 +0200
parents 04876d865528
children
line wrap: on
line source

// This is Free Software under GNU Affero General Public License v >= 3.0
// without warranty, see README.md and license for details.
//
// SPDX-License-Identifier: AGPL-3.0-or-later
// License-Filename: LICENSES/AGPL-3.0.txt
//
// Copyright (C) 2018 by via donau
//   – Österreichische Wasserstraßen-Gesellschaft mbH
// Software engineering by Intevation GmbH
//
// Author(s):
//  * Sascha L. Teichmann <sascha.teichmann@intevation.de>

package wfs

import (
	"encoding/xml"
	"errors"
	"io"
	"regexp"
	"strconv"
	"strings"

	"golang.org/x/net/html/charset"
)

// Keyword stores a value.
type Keyword struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Keyword"`
	Value   string   `xml:",cdata"`
}

// Keywords stores a list of keywords.
type Keywords struct {
	XMLName  xml.Name  `xml:"http://www.opengis.net/ows/1.1 Keywords"`
	Keywords []Keyword `xml:"Keyword"`
}

// ServiceIdentification contains meta informations about a WFS.
type ServiceIdentification struct {
	XMLName            xml.Name `xml:"http://www.opengis.net/ows/1.1 ServiceIdentification"`
	Title              string
	Abstract           string
	Keywords           Keywords `xml:"Keywords"`
	ServiceType        string
	ServiceTypeVersion string
}

// Get stores the link to the GET method
type Get struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Get"`
	HRef    string   `xml:"http://www.w3.org/1999/xlink href,attr"`
}

// Post stores the link to the POST method.
type Post struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Post"`
	HRef    string   `xml:"http://www.w3.org/1999/xlink href,attr"`
}

// HTTP is a container for HTTP methods.
type HTTP struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 HTTP"`
	Get     *Get     `xml:"Get"`
	Post    *Post    `xml:"Post"`
}

// DCP wraps the HTTP container.
type DCP struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 DCP"`
	HTTP    HTTP     `xml:"HTTP"`
}

// Value is a simple string value.
type Value struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 Value"`
	Value   string   `xml:",cdata"`
}

// AllowedValues is list positive list of values.
type AllowedValues struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 AllowedValues"`
	Values  []Value  `xml:"Value"`
}

// Parameter is a named parameter with a list of allowed values.
type Parameter struct {
	XMLName       xml.Name      `xml:"http://www.opengis.net/ows/1.1 Parameter"`
	Name          string        `xml:"name,attr"`
	AllowedValues AllowedValues `xml:"AllowedValues"`
}

// DefaultValue is the default value of a constraint.
type DefaultValue struct {
	XMLName xml.Name `xml:"http://www.opengis.net/ows/1.1 DefaultValue"`
	Value   string   `xml:",cdata"`
}

// Constraint is a named constraint with a list of allowed values
// and a default value.
type Constraint struct {
	XMLName       xml.Name      `xml:"http://www.opengis.net/ows/1.1 Constraint"`
	Name          string        `xml:"name,attr"`
	AllowedValues AllowedValues `xml:"AllowedValues"`
	DefaultValue  *DefaultValue `xml:"DefaultValue"`
}

// Operation contains informations of a WFS operation.
type Operation struct {
	XMLName     xml.Name      `xml:"http://www.opengis.net/ows/1.1 Operation"`
	Name        string        `xml:"name,attr"`
	DCP         DCP           `xml:"DCP"`
	Parameters  []*Parameter  `xml:"Parameter"`
	Constraints []*Constraint `xml:"Constraint"`
}

// OperationsMetadata is list of operations and constraints.
type OperationsMetadata struct {
	XMLName     xml.Name      `xml:"http://www.opengis.net/ows/1.1 OperationsMetadata"`
	Operations  []*Operation  `xml:"Operation"`
	Constraints []*Constraint `xml:"Constraint"`
}

// WGS84BoundingBox is a bounding box feature type in WGS84.
type WGS84BoundingBox struct {
	XMLName     xml.Name `xml:"http://www.opengis.net/ows/1.1 WGS84BoundingBox"`
	LowerCorner string   `xml:"LowerCorner"`
	UpperCorner string   `xml:"UpperCorner"`
}

// FeatureType is layer served by the WFS:
type FeatureType struct {
	XMLName          xml.Name          `xml:"http://www.opengis.net/wfs/2.0 FeatureType"`
	Name             string            `xml:"Name"`
	Title            string            `xml:"Title"`
	Abstract         string            `xml:"Abstract"`
	Keywords         Keywords          `xml:"Keywords"`
	DefaultCRS       string            `xml:"DefaultCRS"`
	OtherCRSs        []string          `xml:"OtherCRS"`
	WGS84BoundingBox *WGS84BoundingBox `xml:"WGS84BoundingBox"`
	Namespaces       []xml.Name        `xml:"-"`
}

// shadowFeatureType is used to prevent recursive UnmarshalXML for FeatureType.
type shadowFeatureType struct {
	XMLName          xml.Name          `xml:"http://www.opengis.net/wfs/2.0 FeatureType"`
	Name             string            `xml:"Name"`
	Title            string            `xml:"Title"`
	Abstract         string            `xml:"Abstract"`
	Keywords         Keywords          `xml:"Keywords"`
	DefaultCRS       string            `xml:"DefaultCRS"`
	OtherCRSs        []string          `xml:"OtherCRS"`
	WGS84BoundingBox *WGS84BoundingBox `xml:"WGS84BoundingBox"`
	Namespaces       []xml.Name        `xml:"-"`
}

// UnmarshalXML implements xml.Unmarshaler for better namespace handling.
func (ft *FeatureType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	// Filter out the namespaces for this feature type.
	var ns []xml.Name
	for _, attr := range start.Attr {
		if attr.Name.Space == "xmlns" {
			ns = append(ns, xml.Name{Space: attr.Name.Local, Local: attr.Value})
		}
	}
	var sft shadowFeatureType
	if err := d.DecodeElement(&sft, &start); err != nil {
		return err
	}
	*ft = FeatureType(sft)
	ft.Namespaces = ns
	return nil
}

// FeatureTypeList is the list of layers served by the WFS.
type FeatureTypeList struct {
	XMLName      xml.Name       `xml:"http://www.opengis.net/wfs/2.0 FeatureTypeList"`
	FeatureTypes []*FeatureType `xml:"FeatureType"`
}

// Capabilities is the top level metadata struct.
type Capabilities struct {
	XMLName xml.Name `xml:"http://www.opengis.net/wfs/2.0 WFS_Capabilities"`

	BaseURL string `xml:"-"`

	ServiceIdentification ServiceIdentification
	OperationsMetadata    OperationsMetadata
	FeatureTypeList       FeatureTypeList
}

// FindOperation searches the capabilities for a specifc operation.
// Returns nil if not found.
func (c *Capabilities) FindOperation(name string) *Operation {
	for _, op := range c.OperationsMetadata.Operations {
		if op.Name == name {
			return op
		}
	}
	return nil
}

// SupportsHits checks if a operation supports the hits request.
func (op *Operation) SupportsHits() bool {
	for _, p := range op.Parameters {
		if p.Name == "resultType" {
			for _, av := range p.AllowedValues.Values {
				if av.Value == "hits" {
					return true
				}
			}
		}
	}
	return false
}

// SupportsOutputFormat checks if one of the given formats is supported.
func (op *Operation) SupportsOutputFormat(formats ...string) string {
	for _, p := range op.Parameters {
		if p.Name == "outputFormat" {
			for _, av := range p.AllowedValues.Values {
				v := strings.ToLower(av.Value)
				for _, f := range formats {
					if v == strings.ToLower(f) {
						return av.Value
					}
				}
			}
		}
	}
	return ""
}

// FeaturesPerPage returns the number of features per page.
// Returns if paging is not supported by the operation.
func (op *Operation) FeaturesPerPage() (int, bool) {
	for _, c := range op.Constraints {
		if c.Name == "CountDefault" {
			if c.DefaultValue != nil {
				if v, err := strconv.Atoi(c.DefaultValue.Value); err == nil {
					return v, true
				}
			}
			for _, av := range c.AllowedValues.Values {
				if v, err := strconv.Atoi(av.Value); err == nil {
					return v, true
				}

			}
		}
	}
	return 0, false
}

// FindFeatureType searches the layers for a given name.
// Returns nil if not found.
func (c *Capabilities) FindFeatureType(name string) *FeatureType {
	for _, ft := range c.FeatureTypeList.FeatureTypes {
		if ft.Name == name {
			return ft
		}
	}
	return nil
}

// FindParameter searches for named parameter. Returns nil
// if not found.
func (op *Operation) FindParameter(name string) *Parameter {
	for _, p := range op.Parameters {
		if p.Name == name {
			return p
		}
	}
	return nil
}

// WFS200 is dotted version string of version 2.0.0.
const WFS200 = "2.0.0"

var versionRe = regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`)

func versionIsLess(a, b string) bool {
	am := versionRe.FindStringSubmatch(a)
	bm := versionRe.FindStringSubmatch(b)

	var n int
	if len(am) < len(bm) {
		n = len(am)
	} else {
		n = len(bm)
	}
	n--

	for i := 0; i < n; i++ {
		ai, _ := strconv.Atoi(am[i+1])
		bi, _ := strconv.Atoi(bm[i+1])
		switch {
		case ai < bi:
			return true
		case ai > bi:
			return false
		}
	}
	return false
}

func maxVersion(a, b string) string {
	am := versionRe.FindStringSubmatch(a)
	bm := versionRe.FindStringSubmatch(b)

	var n int
	if len(am) < len(bm) {
		n = len(am)
	} else {
		n = len(bm)
	}
	n--

	for i := 0; i < n; i++ {
		ai, _ := strconv.Atoi(am[i+1])
		bi, _ := strconv.Atoi(bm[i+1])
		switch {
		case ai > bi:
			return a
		case bi > ai:
			return b
		}
	}
	return a
}

// HighestWFSVersion figures out the highest supported WFS version.
// Defaults to def.
func (c *Capabilities) HighestWFSVersion(def string) string {
	op := c.FindOperation("GetCapabilities")
	if op == nil {
		return def
	}
	p := op.FindParameter("AcceptVersions")
	if p == nil {
		return def
	}
	if len(p.AllowedValues.Values) == 0 {
		return def
	}

	max := p.AllowedValues.Values[0].Value
	for _, v := range p.AllowedValues.Values[1:] {
		max = maxVersion(max, v.Value)
	}

	return max
}

var (
	// ErrInvalidCRS is returned if a given string is not valid CRS URN.
	ErrInvalidCRS = errors.New("invalid CRS string")
	crsRe         = regexp.MustCompile(`urn:ogc:def:crs:EPSG:[^:]*:(\d+)`)
)

// CRSToEPSG extracts the EPSG code from a given CRS URN string.
func CRSToEPSG(s string) (int, error) {
	m := crsRe.FindStringSubmatch(s)
	if m == nil {
		return 0, ErrInvalidCRS
	}
	return strconv.Atoi(m[1])
}

// ParseCapabilities constructs a capabilities document from an io.Reader.
func ParseCapabilities(r io.Reader) (*Capabilities, error) {

	decoder := xml.NewDecoder(r)
	decoder.CharsetReader = charset.NewReaderLabel

	var capabilities Capabilities

	if err := decoder.Decode(&capabilities); err != nil {
		return nil, err
	}

	return &capabilities, nil
}