Category: GIS

  • Extracting legend symbology from ArcGIS Server Map Services

    Our user experience team were looking to do a refresh on our symbology that we use for our Map Services that we host on ArcGIS Server. They wanted to see all symbols used in the map service so they could pass it on to our graphic designer to redesign the icons. While you can see each symbol by adding /legend to the map service URL, there was no way to see all symbols on a single page. There isn’t any native way in ArcGIS Server or Desktop to be able to easily extract this either.

    My first approach was to try and use Javascript to iframe each Map Service legend page into a single HTML page. But due to the same origin policy being set on the web adaptor of our ArcGIS Server machines, this wasn’t feasible.

    Instead, I wrote a Node.JS script to scrape the symbols from ArcGIS Server using Puppeteer. The script handles map services that only have a single symbol vs grouped symbols, and only outputs the unique symbols.

  • Translink GTFS

    I recently became aware that Translink, Queensland’s public transport provider, openly publishes both a GTFS (General Transit Feed Specification) Static dataset and Realtime feed. The GTFS Static dataset provides information on routes, schedules, stops and pathing, while the Realtime feed provides information on current vehicle lat/long coordinates and delay information.

    I created a web app to visualise the realtime feed on a map and supplement it with the static data (direction and vehicle type).

    The initial version used moving markers to simulate vehicle movement. Speed was based both on static timetable information and adjusted using real-time speed (calculated from the distance traveled between updates of the feed). While the moving markers were appealing, the crude design of the web app was such that these calculations were performed by the browser instead of the server to offload processing. This resulted in a lot of data being sent to the browser, and was unsuitable for mobile devices with limited data caps. I ended up removing this functionality when optimising the reduction in data exchanged between the client and server.

    View app in fullscreen
    Source code on GitHub

  • Querying ArcGIS attribute subtypes and domains with SQL

    ArcGIS geodatabases have the concept of domains, where an attribute can be restricted to a set of values, similar to a drop down list. Furthermore, geodatabases also allow an attribute to be specified as a subtype where the subtype determines which set of domains that apply to attributes. This can be used when setting up cascading drop down lists.

    In an Enterprise Geodatabase, this subtype and domain metadata is stored in the GDB_ITEMS table in an XML definition. This ESRI technical article provides code on extracting domain values via SQL:

    SELECT
    items.Name,
    codedValue.value('Code[1]', 'nvarchar(max)') Code,
    codedValue.value('Name[1]', 'nvarchar(max)') Value
    FROM SDE.GDB_ITEMS AS items
    INNER JOIN SDE.GDB_ITEMTYPES AS itemtypes ON items.Type = itemtypes.UUID
    CROSS APPLY items.Definition.nodes('/GPCodedValueDomain2/CodedValues/CodedValue') AS CodedValues(codedValue)
    

    This works well if there are no subtypes, as you can just join your table or view against the Code column and display the Value column. To go one step further an incorporate subtypes, we need to introduce another query that extracts the domains used by a subtype (change MyTable and MyAttribute to appropriate values):

    SELECT
    SubtypeChildren.value('SubtypeCode[1]', 'varchar(50)') SubtypeCode,
    DomainName.value('.', 'varchar(50)') DomainName
    FROM GDB_ITEMS i
    CROSS APPLY i.Definition.nodes('//Subtype') AS Subtype(SubtypeChildren)
    CROSS APPLY SubtypeChildren.nodes('FieldInfos/SubtypeFieldInfo/FieldName[text()="MyAttribute"]/../DomainName') AS SubtypeFieldInfo(DomainName)
    WHERE Name = 'MyTable'
    

    This returns the subtype code as well as the domain name. The subtype code can then be joined against your table or view, while the domain name can be joined against the items.Name from the first query. To be able to return the cascaded domain, we need to join again on the first query. For example, using CTEs:

    WITH domains AS (
    SELECT
    items.Name,
    codedValue.value('Code[1]', 'nvarchar(max)') Code,
    codedValue.value('Name[1]', 'nvarchar(max)') Value
    FROM SDE.GDB_ITEMS AS items
    INNER JOIN SDE.GDB_ITEMTYPES AS itemtypes ON items.Type = itemtypes.UUID
    CROSS APPLY items.Definition.nodes('/GPCodedValueDomain2/CodedValues/CodedValue') AS CodedValues(codedValue)
    ),
    subtypes AS (
    SELECT
    SubtypeChildren.value('SubtypeCode[1]', 'varchar(50)') SubtypeCode,
    DomainName.value('.', 'varchar(50)') DomainName
    FROM GDB_ITEMS i
    CROSS APPLY i.Definition.nodes('//Subtype') AS Subtype(SubtypeChildren)
    CROSS APPLY SubtypeChildren.nodes('FieldInfos/SubtypeFieldInfo/FieldName[text()="MyAttribute"]/../DomainName') AS SubtypeFieldInfo(DomainName)
    WHERE Name = 'MyTable'
    )
    SELECT
    m.Category CategoryCode,
    d1.Value CategoryDescription,
    m.MyAttribute TypeCode,
    d2.Value TypeDescription
    FROM MyTable m
    INNER JOIN domains d1 ON d.Code = m.Category
    INNER JOIN subtypes s ON s.SubtypeCode = m.Category
    INNER JOIN domains d2 ON d2.Code = m.MyAttribute
    

    The subtypes query above is limited to a single attribute as this was the use case I needed to support. To generalise it and return all fields, modify it to return the SubtypeCode, FieldName and DomainName.

  • Creating a ArcGIS map service raster layer in PyQGIS

    It’s relatively straightforward creating layers programatically in QGIS using PyQGIS. However, the documentation isn’t clear on how to use create a raster layer from a ArcGIS map service.

    The list of raster providers can be found by searching for addRasterProviderDialog in qgsdatasourcemanagerdialog.cpp.

    There are two mandatory parts to the data source string when specifying the ArcGIS map service, the url and layer, which need to be space separated. For example:

    iface.addRasterLayer(
    "url='http://gis.infrastructure.gov.au/infrastructure/rest/services/KeyFreightRoute/KFR/MapServer' layer='5'",
    "Key road freight routes", "arcgismapserver"
    )
    

    or using the constructor:

    layer = QgsRasterLayer(
    "url='http://gis.infrastructure.gov.au/infrastructure/rest/services/KeyFreightRoute/KFR/MapServer' layer='5'",
    "Key road freight routes", "arcgismapserver"
    )
    

    To understand what data source parameters are supported by the ArcGIS map service provider (AMSProvider), search for dataSource.Param in qgsamsprovider.cpp.