Creating a My Ward page

Along with the standard My House and My Nearest pages which display information related to a users selected address iShare Maps also supports Area Search pages which display information associated with an area.

The user still searches for an address like normal but when viewing an Area Search page the area which the current address falls within is used to query data. Commonly Area Search pages are used to create My Ward or My Parish pages which can be useful for giving a broader view of a given area.

This tutorial will step through creating a My Ward page using the sample workshop data commonly used for iShare Maps training.

Create a ward lookup

In order to create a My Ward page we first need to create a ward_lookup Shapefile with the ward areas as polygons, the ward name and code. In this instance we are going to use ward polygons derived from OS BoundaryLine which is present in the database as boundaryline.wards_surrey.

Workflow

  • Create a new Workflow Job for the Tasks required to create the data for My Ward called My Ward
  • Create a new Spatial Data Transformation Task Create ward_lookup which converts boundaryline.wards_surrey in the DataShare database to as Shapefile called wards_lookup.shp
  • Run the Task to generate the Shapefile

Mapfile

  • Add a ward_lookup layer to your overlay mapfile for example:
    LAYER
        NAME "ward_lookup"
        STATUS OFF
        TYPE POLYGON
        DATA "Overlays/ward_lookup.shp"
        TOLERANCE 0
        METADATA
            qstring_validation_pattern "."
            qstr_validation_pattern "."
        END
        LABELITEM "name"
        CLASS
            NAME ""
            STYLE
                OUTLINECOLOR 81 167 174
                WIDTH 3
            END
            LABEL
                FONT "verdana"
                TYPE truetype
                SIZE 8
                COLOR 255 0 0
                OUTLINECOLOR 255 255 255
                OUTLINEWIDTH 3
            END
        END
    END


Of particular interest is:

  • TOLERANCE 0
    • All layers used by My Ward will have a TOLERANCE of 0 to ensure only those features that strictly intersect when searching are returned
  • The layer is styled and includes a label which we will use later when displaying a ward map

Create the My Ward page

  • Create a new My Ward MapSource
    • Call the file MyWard.xml
    • Delete all but one of the existing groups
  • Select the main node to display the high level MapSource properties
  • Ensure the MapFile Path is set to the path to your overlays mapfile
  • Select the My Ward item in the lefthand list of MapSources then:
    • Select the Nearest page Page Type
    • Select the Area Search Search Type
    • Select ward_lookup as the Layer Name
    • Select code as the Search Field
  • Create a file called MyWard_ward_lookup_fields.xml in the _MapServerConfig\_MapServerTemplates folder with the following contents:

        <!-- MapServer Template -->
        <row LayerName="ward_lookup">
            <name><![CDATA[[name]]]></name>
            <code><![CDATA[[code]]]></code>
            <MapSpurE>[shpmidx]</MapSpurE>
            <MapSpurN>[shpmidy]</MapSpurN>
            <MapSpurMinE>[shpminx]</MapSpurMinE>
            <MapSpurMinN>[shpminy]</MapSpurMinN>
            <MapSpurMaxE>[shpmaxx]</MapSpurMaxE>
            <MapSpurMaxN>[shpmaxy]</MapSpurMaxN>
        </row>

Display the ward name

So we've something to display we will create a Ward name layer.

Mapfile

  • Create a new ward_summary layer in overlays.map which queries the boundaryline.wards_surrey layer in the iShareData database.

        LAYER
            NAME "ward_summary"
            STATUS OFF
            TYPE POINT
            INCLUDE "_datashare.map"
            DATA "wkb_geometry from (select ogc_fid, code, name, ST_PointOnSurface(wkb_geometry) as wkb_geometry from boundaryline.wards_surrey) as foo using unique ogc_fid using srid=27700"
            TOLERANCE 0
            METADATA
                qstring_validation_pattern "."
                qstr_validation_pattern "."
            END
            CLASS
                NAME ""
                STYLE
                    SYMBOL "circle"
                    COLOR 0 128 0
                    SIZE 8
                END
            END
        END


Of note is the SQL used in the DATA statement which creates a row per ward with a point within each ward polygon which will be used for the lookup. The PostGIS function ST_PointOnSurface returns a POINT guaranteed to lie on the surface of a polygon.

        SELECT
            ogc_fid,
            code,
            name,
            ST_PointOnSurface(wkb_geometry) AS wkb_geometry
        FROM boundaryline.wards_surrey

Studio

  • Create a Summary Layer Group
    • Ensure Visible on Startup is checked
  • Create a Ward name Layer
    • Select the ward_summary Layer Name
    • Ensure the Show My Search and Show My option is checked
    • Choose the name field

It may be necessary to restart the iShare Web and WebService application pools in order to see the new My Ward page.

The My Ward page now simply displays the name of the ward that the current address falls within, not very excited but a start.


Adding a list of planning applications

Next we will display a list of planning applications which fall within the current ward. As our planning applications table contains POINT geometries we can simply use the data as is without the need for any Workflow.

Mapfile

  • Add a ward_planning layer to overlays.map which queries planning.surrey_planning from the iShareData database. Again notice that the layer has a TOLERANCE of 0.

        LAYER
            NAME "ward_planning"
            STATUS OFF
            TYPE POINT
            INCLUDE "_datashare.map"
            DATA "wkb_geometry from (select * from planning.surrey_planning) as foo using unique ogc_fid using srid=27700"
            TOLERANCE 0
            METADATA
                qstring_validation_pattern "."
                qstr_validation_pattern "."
            END
            CLASS
                NAME ""
                STYLE
                    SYMBOL "circle"
                    COLOR 0 128 0
                    SIZE 8
                END
            END
        END

Studio

  • Create a Planning Layer Group
    • Ensure Visible on Startup is checked
  • Create a Planning applications Layer
    • Select the ward_planning Layer Name
    • Ensure the Show MySearch and Show My option is checked
    • Choose the title and description fields

Planning application summary

As well as simply displaying the list of planning applications we can also display a summary giving a count of planning applications.

Studio - Workflow

  • Create a new Stored Procedure Task called Create ward_planning_summary, choose:
    • Database: DataShare
    • Function: at_sys_create_table from the Astun category (from v6.0.0 this is called wkf_create_table under the -Workflow- Function filter)
    • Set the tablename parameter to: ward_planning_summary and the selectstatement to:
        WITH counts AS (
            SELECT wards.code,
                count(*) AS count
            FROM planning.surrey_planning planning,
                boundaryline.wards_surrey wards
            WHERE st_intersects(wards.wkb_geometry, planning.wkb_geometry)
            GROUP BY wards.code
        )
        SELECT wards.ogc_fid,
            wards.code,
            st_pointonsurface(wards.wkb_geometry) AS wkb_geometry,
            'There ' ||
            CASE WHEN counts.count = 1 THEN 'is ' ELSE 'are ' END ||
            coalesce(counts.count::text, 'no') ||
            ' planning application' ||
            CASE WHEN counts.count = 1 THEN '' ELSE 's' END AS summary
        FROM boundaryline.wards_surrey wards
        LEFT JOIN counts ON (wards.code = counts.code)


The SQL first creates a counts result set which consists of a row per ward with it's code and a count of planning applications which fall within it. This counts lookup is then joined to the wards table on ward code, the results contain ward code, a POINT within the ward, and a html column which contains a formatted summary for example: "There are 3 planning applications".

Mapfile

  • Create a ward_planning_summary layer in overlays.map


        LAYER
            NAME "ward_planning_summary"
            STATUS OFF
            TYPE POINT
            INCLUDE "_datashare.map"
            DATA "wkb_geometry from (select * from ward_planning_summary) as foo using unique ogc_fid using srid=27700"
            TOLERANCE 0
            METADATA
                qstring_validation_pattern "."
                qstr_validation_pattern "."
            END
            CLASS
                NAME ""
                STYLE
                    SYMBOL "circle"
                    COLOR 0 128 0
                    SIZE 8
                END
            END
        END

Studio - MapSource

  • Create a Planning summary layer below the Summary group
    • Select the ward_planning_summary Layer Name
    • Ensure the Show MySearch and Show My option is checked
    • Choose the summary field

Create a ward map

Currently all data is shown in My Nearest panels but lets add a map showing the current ward then update the page style via CSS to display the map and summary panels side by side at the top of the page followed by all other panels as My Nearest panels below.

Studio - Workflow

First we're going to create a table which has a row per ward with a POINT within the ward and a html field containing an img element with the appropriate src URL to generate a map zoomed to the extent of the ward.

  • Create a new Stored Procedure Task called Create ward_map, choose:
    • Database: DataShare
    • Function: at_sys_create_table from the Astun category (from v6.0.0 this is called wkf_create_table under the -Workflow- Function filter)
    • Set the tablename parameter to: ward_map and the selectstatement to:
    WITH envelope AS (
        SELECT
            ogc_fid,
            wkb_geometry,
            code,
            name,
            st_expand(st_envelope(wkb_geometry), 300) AS envelope
        FROM boundaryline.wards_surrey wards
    ),
    coords AS (
        SELECT *,
            st_x(st_centroid(envelope.envelope))::int AS x,
            st_y(st_centroid(envelope.envelope))::int AS y,
            greatest((st_xmax(envelope) - st_xmin(envelope)), (st_ymax(envelope) - st_ymin(envelope)))::int AS z
        FROM envelope
    )
    SELECT
        ogc_fid,
        code,
        name,
        st_pointonsurface(wkb_geometry) AS wkb_geometry,
        '<img class="map" src="MapGetImage.aspx?RequestType=Map&MapWidth=450&MapHeight=450&MapSource=Astun/Default&Easting=' || x || '&Northing=' || y || '&Zoom=' || z || '&mapid=-1&ServiceAction=ZoomToLocation&Layers=ward_lookup,ward_summary" alt="' || name || '" />' AS html
    FROM coords


The SQL first creates an envelope result set which includes a geometry for the bounds of each ward, the coords result set then extracts the centre (x/y) and width (z) and finally a result set uses these values to construct a POINT within the ward and a html field containing an img element with the appropriate src URL to generate a map zoomed to the extent of the ward. The URL specifies that the ward_lookuplayer is displayed on the map so that the boundaries are visible together with labels.

Mapfile

LAYER
    NAME "ward_map"
    STATUS OFF
    TYPE POINT
    INCLUDE "_datashare.map" 
    DATA "wkb_geometry from (select *, html as html_raw from ward_map) as foo using unique ogc_fid using srid=27700"
    TOLERANCE 0
    METADATA
        qstring_validation_pattern "."
        qstr_validation_pattern "."
    END
    CLASS
        NAME ""
        STYLE
            SYMBOL "circle"
            COLOR 0 128 0
            SIZE 8
        END  
    END
END


Of note is the SQL used in the DATA statement which creates html_raw column by aliasing the existing html column. This allows us to use the html_raw column for the MapSource layer causing the raw HTML text to be output which will cause the img to be displayed.

Studio - MapSource

  • Create a Ward map Layer Group
    • Ensure Visible on Startup is checked
    • Move it up so it's the first Layer Group with the Summary Layer Group second
  • Create a Ward map Layer
    • Select the ward_map Layer Name
    • Ensure the Show MySearch and Show My option is checked
    • Choose the html_raw field and ensure Display field name is unchecked

Styling

To display the map and summary panels side by side at the top of the page we need a little JavaScript and CSS.

JavaScript

In order to allow us identify the first and second panels on the My Ward page we will use a little JavaScript to:

  • Add a class to the page body element to allow us to identify which page is currently being displayed
  • Add a class to each panel with it's position in the page
  • Disable ability to collapse the first two panels

    window.astun.customJS = function() {

        // Determine which page we are on from the selected tab
        var pageName = jQuery('.atTabSelected').val().replace(/\s/, '_').toLowerCase();

        // Add a class to the body to allow us to create specific rules for individual pages
        jQuery('body').addClass(pageName);

        // Add a class to each panel with it's position in the page,
        // atPanel0, atPanel1 etc.
        jQuery('.atPanelContainer .atPanel').each(function(idx) {
            jQuery(this).addClass('atPanel' + idx);
        });

        if (pageName === 'my_ward') {
            // Make the first two panels on the My Ward page non-collapsable
            jQuery('.atPanel.atPanel0, .atPanel.atPanel1').find("*").unbind();
            // Remove the "Click to toggle" tooltip
            jQuery('.atPanel.atPanel0, .atPanel.atPanel1').find(".atPanelHeader").attr("title", "");
        }

    };

CSS

The following CSS when added to one of the site specific CSS files found under the Web\custom directory applies the following styles:

  • Display the first two panel side by side and always expanded
  • Hide the headings for the summary panel
  • Hide the "Details" and "Distance" table headings
  • Make the ward map responsive


    /* Position the first two My Ward panels */
    .my_ward .atPanel.atPanel0, .my_ward  .atPanel.atPanel1 {
        width: 48%;
        float:left;
        margin-left: 10px;
    }

    /* Always display the content of the first two panels */
    .my_ward .atPanel.atPanel0 .atPanelParentContent, .my_ward  .atPanel.atPanel1 .atPanelParentContent {
        display: block !important;
    }

    /* Ensure the third panel displays below the first two */
    .my_ward .atPanel.atPanel0, .my_ward  .atPanel.atPanel2 {
        clear: both;
    }

    /* Style headers for the first two panels */

    .my_ward .atPanel.atPanel0 .atPanelHeader, .my_ward .atPanel.atPanel1 .atPanelHeader {
        cursor: default !important;    
    }
    .my_ward .atPanel.atPanel0 .atPanelStatusSign, .my_ward  .atPanel.atPanel1 .atPanelStatusSign {
        visibility: hidden;
    }

    /* Style panel content */

    /* Hide layer titles for first two panels */
    .my_ward .atPanel.atPanel0 h4, .my_ward  .atPanel.atPanel1 h4 {
        display: none;
    }

    .my_ward table thead {
        display: none;
    }
    .my_ward table tr th:nth-child(1),
    .my_ward table tr td:nth-child(1) {
        width: 100%;
    }

    /* Make the ward map responsive */
    .my_ward  img.map {
        width: 100%;
    }

Responsive design

As a bonus lets make some small changes to make the My Ward responsive and display appropriately on a range of device sizes. In this case we can simply add a single media query to our CSS to apply a different style to the first two panels when the screen width is smaller than a given size.

@media (max-width: 640px) {
    
	.my_ward .atPanel.atPanel0, .my_ward  .atPanel.atPanel1 {
		width: 100%;
		margin-left: 0;
	}

}