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 WardCreate a new Spatial Data Transformation Task
Create ward_lookupwhich convertsboundaryline.wards_surreyin theDataSharedatabase to as Shapefile calledwards_lookup.shpRun the Task to generate the Shapefile
Mapfile
Add a
ward_lookuplayer 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 0All layers used by My Ward will have a
TOLERANCEof0to 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 WardMapSourceCall the file
MyWard.xmlDelete 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 Warditem in the lefthand list of MapSources then:Select the
Nearest pagePage TypeSelect the
Area SearchSearch TypeSelect
ward_lookupas the Layer NameSelect
codeas the Search Field
Create a file called
MyWard_ward_lookup_fields.xmlin the_MapServerConfig\_MapServerTemplatesfolder 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_summarylayer inoverlays.mapwhich queries theboundaryline.wards_surreylayer in theiShareDatadatabase.
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_surreyStudio
Create a
SummaryLayer GroupEnsure Visible on Startup is checked
Create a
Ward nameLayerSelect the
ward_summaryLayer NameEnsure the Show My Search and Show My option is checked
Choose the
namefield
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_planninglayer tooverlays.mapwhich queriesplanning.surrey_planningfrom theiShareDatadatabase. Again notice that the layer has aTOLERANCEof0.
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
ENDStudio
Create a
PlanningLayer GroupEnsure Visible on Startup is checked
Create a
Planning applicationsLayerSelect the
ward_planningLayer NameEnsure the Show MySearch and Show My option is checked
Choose the
titleanddescriptionfields
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:
DataShareFunction:
at_sys_create_tablefrom 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_summaryand 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_summarylayer inoverlays.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
ENDStudio - MapSource
Create a
Planning summarylayer below theSummarygroupSelect the
ward_planning_summaryLayer NameEnsure the Show MySearch and Show My option is checked
Choose the
summaryfield
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:
DataShareFunction:
at_sys_create_tablefrom the Astun category (from v6.0.0 this is called wkf_create_table under the -Workflow- Function filter)Set the tablename parameter to:
ward_mapand 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 mapLayer GroupEnsure Visible on Startup is checked
Move it up so it's the first Layer Group with the
SummaryLayer Group second
Create a
Ward mapLayerSelect the
ward_mapLayer NameEnsure the Show MySearch and Show My option is checked
Choose the
html_rawfield 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;
}
}