import re
import math
import urllib.parse
import gws
import gws.types as t
import gws.tools.xml2
import gws.tools.net
import gws.common.ows.provider.parseutil as u
import gws.gis.source
from . import types
_bigval = 1e10
[docs]def parse(prov, xml):
root = gws.tools.xml2.from_string(xml)
prov.properties = _properties(root.first('properties'))
prov.meta = _project_meta_from_props(prov.properties)
prov.version = root.attr('version', '').split('-')[0]
if prov.version.startswith('2'):
prov.print_templates = _print_v2(root)
if prov.version.startswith('3'):
prov.print_templates = _print_v3(root)
for n, cc in enumerate(prov.print_templates):
cc.index = n
map_layers = _map_layers(root, prov.properties)
root_group = _tree(root.first('layer-tree-group'), map_layers)
prov.source_layers = u.flatten_source_layers(root_group.layers)
crs = None
if prov.version.startswith('2'):
crs = _pval(prov.properties, 'SpatialRefSys.ProjectCrs')
if prov.version.startswith('3'):
crs = root.get_text('projectCrs.spatialrefsys.authid')
if crs:
prov.supported_crs = [crs]
def _project_meta_from_props(props):
p = gws.strip({
'abstract': _pval(props, 'WMSServiceAbstract'),
'attribution': _pval(props, 'CopyrightLabel.Label'),
'keywords': _pval(props, 'WMSKeywordList'),
'title': _pval(props, 'WMSServiceTitle'),
})
if not p:
return
meta = t.MetaData(p)
p = gws.strip({
'email': _pval(props, 'WMSContactMail'),
'organization': _pval(props, 'WMSContactOrganization'),
'person': _pval(props, 'WMSContactPerson'),
'phone': _pval(props, 'WMSContactPhone'),
'position': _pval(props, 'WMSContactPosition'),
})
if p:
meta.contact = t.MetaContact(p)
return meta
def _pval(props, key):
return gws.get(props, key.lower())
def _properties(el):
# NB: property keys converted to lowercase, cased args to _pval are just for readability
et = el.attr('type')
if not et:
return gws.strip({e.name.lower(): _properties(e) for e in el.all()})
if et == 'int':
return int(el.text or '0')
if et == 'QStringList':
return gws.strip([e.text for e in el.all()])
if et == 'QString':
return el.text
if et == 'bool':
return el.text.lower() == 'true'
if et == 'double':
return _float(el.text)
def _tree(el, map_layers):
visible = el.attr('checked') != 'Qt::Unchecked'
expanded = el.attr('expanded') == '1'
if el.name == 'layer-tree-group':
title = el.attr('name')
# qgis doesn't write 'id' for groups but our generators might
name = el.attr('id') or title
sl = t.SourceLayer(title=title, name=name)
sl.meta = t.MetaData(title=title, name=name)
sl.is_visible = visible
sl.is_expanded = expanded
sl.is_group = True
sl.is_queryable = False
sl.is_image = False
sl.layers = gws.compact(_tree(e, map_layers) for e in el.all())
return sl
if el.name == 'layer-tree-layer':
sl = map_layers.get(el.attr('id'))
if sl:
sl.is_visible = visible
sl.is_expanded = expanded
sl.is_group = False
sl.is_image = True
return sl
def _map_layers(root, props):
disabled_layers = set(_pval(props, 'Identify.disabledLayers') or [])
no_wms_layers = set(_pval(props, 'WMSRestrictedLayers') or [])
use_layer_ids = _pval(props, 'WMSUseLayerIDs')
map_layers = {}
for el in root.all('projectlayers.maplayer'):
sl = _map_layer(el)
if not sl:
continue
# no_wms_layers always contains titles, not ids (=names)
if sl.meta.title in no_wms_layers:
continue
# ggis2: non-queryable layers are on the identify.disabledlayers list
# ggis3: non-queryable layers have <flags><Identifiable>0
s = el.get_text('flags.Identifiable')
if s == '1':
sl.is_queryable = True
elif s == '0':
sl.is_queryable = False
else:
sl.is_queryable = sl.meta.name not in disabled_layers
sl.title = sl.meta.title
sl.name = el.get_text('id') if use_layer_ids else (el.get_text('shortname') or el.get_text('layername'))
sl.meta.name = sl.name
map_layers[el.get_text('id')] = sl
return map_layers
def _layer_meta(el):
p = gws.strip({
'abstract': el.get_text('resourceMetadata.abstract'),
'keywords': gws.compact(e.text for e in el.all('keywordList.value')),
'title': el.get_text('layername'),
'name': el.get_text('id'),
'url': el.get_text('metadataUrl'),
})
if not p:
return
meta = t.MetaData(p)
p = gws.strip({
k: el.get_text('resourceMetadata.contact.' + k)
for k in ('name', 'organization', 'position', 'voice', 'fax', 'email', 'role')
})
if p:
meta.contact = t.MetaContact(p)
return meta
def _map_layer(el):
sl = t.SourceLayer()
sl.meta = _layer_meta(el)
crs = el.get_text('srs.spatialrefsys.authid')
sl.supported_crs = [crs]
sl.supported_bounds = []
e = el.first('extent')
if e:
sl.supported_bounds.append(t.Bounds(
crs=crs,
extent=(
_float(e.get_text('xmin')),
_float(e.get_text('ymin')),
_float(e.get_text('xmax')),
_float(e.get_text('ymax')),
)
))
if el.attr('hasScaleBasedVisibilityFlag') == '1':
# sic! these are called min-max in qgis2 and max-min in qgis3
a = _float(el.attr('minimumScale') or el.attr('maxScale'))
z = _float(el.attr('maximumScale') or el.attr('minScale'))
if z > a:
sl.scale_range = [a, z]
prov = el.get_text('provider').lower()
ds = _data_source(prov, el.get_text('datasource'))
if 'provider' not in ds:
ds['provider'] = prov
sl.data_source = ds
s = el.get_text('layerTransparency')
if s:
sl.opacity = 1 if s == '0' else (100 - int(s)) / 100
s = el.get_text('layerOpacity')
if s:
sl.opacity = _float(s)
return sl
"""
print templates in qgis-2:
<Composer title="..."
<Composition ...
<ComposerPicture elementAttrs...
<ComposerItem itemAttrs...
other tags
<ComposerArrow elementAttrs...
<ComposerItem itemAttrs...
other tag
<Composer title="..."
etc
we merge elementAttrs+itemAttrs and ignore other tags within elements
"""
def _print_v2(root):
return [_print_v2_composer(el) for el in root.all('Composer')]
def _print_v2_composer(composer):
oo = types.PrintTemplate()
oo.title = composer.attr('title', '')
composition = composer.first('Composition')
oo.attrs = _lower_attrs(composition)
oo.elements = [
_print_v2_element(el)
for el in composition.all()
if el.name.startswith('Composer')
]
return oo
def _print_v2_element(el):
oo = types.PrintTemplateElement()
oo.type = el.name[len('Composer'):].lower()
oo.attrs = _lower_attrs(el)
for item in el.all():
if item.name == 'ComposerItem':
oo.attrs.update(_lower_attrs(item))
return oo
"""
print templates in qgis-3:
<Layouts>
<Layout name="..." ...
<PageCollection...
<LayoutItem ....
<LayoutItem type="<int, see below>" ...
other tags
<LayoutItem type="<int>" ...
other tags
<Layout name="..." ...
etc
"""
# see QGIS3/QGIS/src/core/layout/qgslayoutitemregistry.h
_QGraphicsItem_UserType = 65536 # https://doc.qt.io/qtforpython/PySide2/QtWidgets/QGraphicsItem.html
_COMP3_LAYOUT_TYPE_FIRST = _QGraphicsItem_UserType + 100
_COMP3_LAYOUT_TYPES = {
_COMP3_LAYOUT_TYPE_FIRST + n: s for n, s in enumerate(
[
'LayoutItem',
'LayoutGroup',
'LayoutPage',
'LayoutMap',
'LayoutPicture',
'LayoutLabel',
'LayoutLegend',
'LayoutShape',
'LayoutPolygon',
'LayoutPolyline',
'LayoutScaleBar',
'LayoutFrame',
'LayoutHtml',
'LayoutAttributeTable',
'LayoutTextTable',
])
}
def _print_v3(root):
return [_print_v3_layout(el) for el in root.all('Layouts.Layout')]
def _print_v3_layout(layout):
oo = types.PrintTemplate()
oo.title = layout.attr('name', '')
oo.attrs = _lower_attrs(layout)
oo.elements = [_print_v3_item(item) for item in layout.all('PageCollection.LayoutItem')]
oo.elements.extend(_print_v3_item(item) for item in layout.all('LayoutItem'))
return oo
def _print_v3_item(el):
oo = types.PrintTemplateElement()
oo.type = _COMP3_LAYOUT_TYPES[int(el.attr('type'))][len('Layout'):].lower()
oo.attrs = _lower_attrs(el)
return oo
def _data_source(provider, source):
if provider == 'wfs':
params = _parse_datasource_uri(source)
url = params.pop('url', '')
if not url:
return {}
p = gws.tools.net.parse_url(url)
typename = params.pop('typename', '') or p['params'].get('typename')
return {
'url': url,
'typeName': typename,
'params': params
}
if provider == 'wms':
options = {}
for k, v in urllib.parse.parse_qs(source).items():
options[k] = v[0] if len(v) < 2 else v
layers = []
if 'layers' in options:
layers = options.pop('layers')
# 'layers' must be a list
if isinstance(layers, str):
layers = [layers]
url = options.pop('url', '')
params = {}
if url:
url, params = _parse_url_with_qs(url)
d = {'url': url, 'options': options, 'params': params, 'layers': layers}
if 'tileMatrixSet' in options:
d['provider'] = 'wmts'
return d
if provider in ('gdal', 'ogr'):
return {'path': source}
if provider == 'postgres':
return gws.compact(_parse_datasource_uri(source))
return {'source': source}
def _parse_url_with_qs(url):
p = urllib.parse.urlparse(url)
params = {}
if p.query:
params = {k: v for k, v in urllib.parse.parse_qsl(p.query)}
url = urllib.parse.urlunparse(p[:3] + ('', '', ''))
return url, params
def _parse_datasource_uri(uri):
# see QGIS/src/core/qgsdatasourceuri.cpp... ;(
#
# the format appears to be key = value pairs, where value can be quoted and c-escaped
# 'table=' is special, is can be table="foo" or table="foo"."bar" or table="foo"."bar" (geom)
# 'sql=' is special too and can contain whatever, it's always the last one
#
# alternatively, a datasource can be an url
value_re = r'''(?x)
" (?: \\. | [^"])* " |
' (?: \\. | [^'])* ' |
\S+
'''
parens_re = r'\(.*?\)'
def _cut(u, rx):
m = re.match(rx, u)
if not m:
raise ValueError(f'datasource uri error, expected {rx!r}, found {u[:25]!r}')
v = m.group(0)
return v, u[len(v):].strip()
def _unesc(s):
return re.sub(r'\\(.)', '\1', s)
def _mid(s):
return s[1:-1].strip()
def _value(v):
if v.startswith(('\'', '\"')):
return _unesc(_mid(v))
return v
def _parse(u, r):
while u:
# keyword=
key, u = _cut(u, r'^\w+\s*=\s*')
key = key.strip('= ')
# sql=rest...
if key == 'sql':
r[key] = u
break
elif key == 'table':
# table=schema.tab(geom)
v1, u = _cut(u, value_re)
v1 = _value(v1)
v2 = v3 = ''
if u.startswith('.'):
v2, u = _cut(u[1:], value_re)
v2 = _value(v2)
if u.startswith('('):
v3, u = _cut(u, parens_re)
v3 = _mid(v3)
r['table'] = (v1 + '.' + v2) if v2 else v1
if v3:
r['geometryColumn'] = v3
else:
# just param=val
v, u = _cut(u, value_re)
r[key] = _value(v)
if uri.startswith(('http://', 'https://')):
return {'url': uri}
rec = {}
_parse(uri, rec)
return rec
def _lower_attrs(el):
return {k.lower(): v for k, v in el.attr_dict.items()}
def _float(s):
try:
x = float(s)
except:
return 0
if math.isnan(x) or math.isinf(x):
return 0
return x