"""
An ESRI feature "parser" (really the requests library does most of the actual parsing).
Most of the utility functions are exposed but most applications won't use them
:func:make_node is generally the only point of interest here.
"""
import requests, metadata
_proxies = {}
[docs]def make_grid_col( **kw ):
"""
Generate a RAMP compliant datagrid column object with the following defaults:
fieldName ''
isSortable False
sortType 'string'
alignment 0
:param kw: Takes keyword arguments and just fills in the defaults
:returns: dict -- a dictionary with the defaults applied
"""
d = { 'fieldName':'', 'orderable':False, 'type':'string', 'alignment':0 }
d.update(kw)
return d
[docs]def make_extent( json_data ):
"""
Extracts the extent for the layer from ESRI's JSON config.
:param json_data: A dictionary containing scraped data from an ESRI feature service endpoint
:type json_data: dict
:returns: dict -- A dictionary with the same data as the ESRI layerExtent node
"""
return json_data['extent']
[docs]def make_data_grid( json_data ):
"""
Generate a RAMP datagrid by walking through the attributes.
Iterates over all entries in *fields* that do not have a type of *esriFieldTypeGeometry*
:param json_data: A dictionary containing scraped data from an ESRI feature service endpoint
:type json_data: dict
:returns: dict -- A dictionary with a single entry *gridColumns* containing an array of datagrid objects
"""
g = []
g.append( make_grid_col(id="iconCol", width="50px", title="Icon", columnTemplate="graphic_icon") )
g.append( make_grid_col(id="detailsCol", width="60px", title="Details", columnTemplate="details_button") )
g.extend( [ make_grid_col(id=attrib['name'], fieldName=attrib['name'], width="400px",
orderable=True, alignment=1, title=attrib['name'],
columnTemplate="unformatted_grid_value")
for attrib in json_data['fields'] if attrib['type'] != 'esriFieldTypeGeometry' ] )
return { 'gridColumns':g }
[docs]def get_base_url( feature_service_url ):
"""
Strips trailing / from the feature service URL if present.
:param feature_service_url: A URL pointing to an ESRI feature service
:type feature_service_url: str
:returns: str -- A URL pointing to the base URL
"""
if feature_service_url.endswith('/'):
return feature_service_url[:-1]
return feature_service_url
[docs]def get_legend_url( feature_service_url ):
"""
Converts a feature service URL into a legend request. Handles the optional '/' at the end of requests.
:param feature_service_url: A URL pointing to an ESRI feature service
:type feature_service_url: str
:returns: str -- A URL pointing to a legend request
"""
feature_service_url = get_base_url( feature_service_url )
return feature_service_url[:feature_service_url.rfind('/')] + '/legend?f=json'
[docs]def get_legend_mapping( data, layer_id ):
"""
Generates a mapping of layer labels to image data URLs.
:param data: The initial payload to RCS (should contain a 'service_url' entry)
:type data: dict
:param layer_id: The id of the layer to create the mapping for.
:returns: dict -- a mapping of 'label' => 'data URI encoded image'
"""
global _proxies
legend_json = requests.get(get_legend_url( data['service_url'] ), proxies=_proxies).json()
for layer in legend_json['layers']:
if layer['layerId'] == layer_id:
break
return { x['label']:'data:'+x['contentType']+';base64,'+x['imageData'] for x in layer['legend'] }
[docs]def make_alias_mapping( json_data ):
"""
Generates a mapping of field names to field aliases.
:param json_data: An array of field objects, taken from the fields property of an ESRI feature service endpoint
:type json_data: list
:returns: dict -- a mapping of 'name' => 'alias'
"""
return { x['name']:x['alias'] for x in json_data }
[docs]def make_symbology( json_data, data ):
"""
Generates a symbology node for the RAMP configuration. Handles simple,
unique value and class break renders; prefetches all symbology images.
:param json_data: A dictionary containing scraped data from an ESRI feature service endpoint
:type json_data: dict
:param data: The initial payload to RCS (should contain a 'service_url' entry)
:type data: dict
:returns: dict -- a symbology node
"""
render_json = json_data['drawingInfo']['renderer']
symb = { 'type':render_json['type'] }
label_map = get_legend_mapping( data, json_data['id'] )
if render_json['type'] == 'simple':
symb['imageUrl'] = label_map[render_json['label']]
symb['label'] = render_json['label']
elif render_json['type'] == 'uniqueValue':
if render_json.get('defaultLabel',None) and render_json['defaultLabel'] in label_map:
symb['defaultImageUrl'] = label_map[render_json['defaultLabel']]
symb['label'] = render_json['defaultLabel']
for field in 'field1 field2 field3'.split():
symb[field] = render_json[field]
val_maps = [ dict( value= u['value'], imageUrl= label_map[u['label']], label= u['label'] )
for u in render_json['uniqueValueInfos'] ]
symb['valueMaps'] = val_maps
elif render_json['type'] == 'classBreaks':
if render_json.get('defaultLabel',None) and render_json['defaultLabel'] in label_map:
symb['defaultImageUrl'] = label_map[render_json['defaultLabel']]
symb['label'] = render_json['defaultLabel']
symb['field'] = render_json['field']
symb['minValue'] = render_json['minValue']
range_maps = [ dict(maxValue=u['classMaxValue'], imageUrl=label_map[u['label']], label= u['label'] )
for u in render_json['classBreakInfos'] ]
symb['rangeMaps'] = range_maps
return symb
[docs]def test_small_layer( svc_url, svc_data ):
"""
Test a service endpoint to see if the layer is small based on some simple rules.
:param svc_url: The URL pointing to the feature endpoint
:type svc_url: str
:param svc_data: A dictionary containing scraped data from an ESRI feature service endpoint
:type svc_data: dict
:returns: bool -- True if the layer is considered 'small'
"""
# FIXME needs refactoring, better error handling and better logic
global _proxies
try:
if svc_data['geometryType'] in ('esriGeometryPoint','esriGeometryMultipoint','esriGeometryEnvelope'):
count_query = '/query?where=1%3D1&returnCountOnly=true&f=pjson'
id_query = '/query?where=1%3D1&returnIdsOnly=true&f=json'
r = requests.get( get_base_url(svc_url) + count_query, proxies=_proxies)
if 'count' in r.json():
return r.json()['count'] <= 2000
r = requests.get( get_base_url(svc_url) + id_query, proxies=_proxies)
if 'objectIds' in r.json():
return len(r.json()['objectIds']) <= 2000
except:
pass
return False
[docs]def make_node( data, id, config ):
"""
Generate a RAMP layer entry for an ESRI feature service.
:param data: The initial payload to RCS (should contain a 'service_url' entry)
:type data: dict
:param id: An identifier for the layer (as this is unique it is generally supplied from :module:rcs )
:type id: str
:returns: dict -- a RAMP configuration fragment representing the ESRI layer
"""
node = { 'id': id }
global _proxies
if 'FEATURE_SERVICE_PROXY' in config:
_proxies = { 'http': config['FEATURE_SERVICE_PROXY'], 'https': config['FEATURE_SERVICE_PROXY']}
r = requests.get( data['service_url'] + '?f=json', proxies=_proxies )
svc_data = r.json()
node['url'] = data['service_url']
node['displayName'] = data.get('service_name',None)
node['nameField'] = data.get('display_field',None)
if node.get('displayName',None) is None:
node['displayName'] = svc_data['name']
if node.get('nameField',None) is None:
node['nameField'] = svc_data['displayField']
metadata_url, catalogue_url = metadata.get_url( data, config )
if metadata_url:
node['metadataUrl'] = metadata_url
node['catalogueUrl'] = catalogue_url
node['minScale'] = svc_data.get('minScale',0)
node['maxScale'] = svc_data.get('maxScale',0)
node['datagrid'] = make_data_grid( svc_data )
node['layerExtent'] = make_extent( svc_data )
node['symbology'] = make_symbology( svc_data, data )
node['aliasMap'] = make_alias_mapping( svc_data['fields'] )
if 'max_allowable_offset' in data:
node['maxAllowableOffset'] = data['max_allowable_offset']
if 'loading_mode' in data:
node['mode'] = data['loading_mode']
elif test_small_layer( node['url'], svc_data ):
node['mode'] = 'snapshot'
node['geometryType'] = svc_data['geometryType']
return node