Skip to content

Commit 9649255

Browse files
committed
Add required JavaScript
1 parent 8ff86e4 commit 9649255

File tree

5 files changed

+365
-1
lines changed

5 files changed

+365
-1
lines changed

app/assets/config/manifest.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
//= link dependencies.js
55
//= link test-dependencies.js
66
//= link views/travel-advice.js
7+
//= link views/landing_page/map/main.js
8+
//= link views/landing_page/map/os-api-branding.js
9+
//= link views/landing_page/map/data/cdc.geojson.js
10+
//= link views/landing_page/map/data/hub.geojson.js
11+
//= link views/landing_page/map/data/icb.geojson.js
12+
//= link views/landing_page/map/data/lookup.json.js
713

814
//= link static-error-pages.js
915

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/* global L */
2+
//= require leaflet
3+
//= require views/landing_page/map/data/cdc.geojson.js
4+
//= require views/landing_page/map/data/hub.geojson.js
5+
//= require views/landing_page/map/data/icb.geojson.js
6+
//= require views/landing_page/map/data/lookup.json.js
7+
8+
window.addEventListener('DOMContentLoaded', function () {
9+
const mapElement = document.getElementById('map')
10+
if (!mapElement) {
11+
return
12+
}
13+
14+
const apiKey = mapElement.getAttribute('data-api-key')
15+
16+
if (!apiKey) {
17+
const warning = document.querySelector('#api-warning')
18+
warning.innerText = "Error: Map API key not found."
19+
return
20+
}
21+
22+
// Initialize the map.
23+
const mapOptions = {
24+
minZoom: 7,
25+
maxZoom: 16,
26+
center: [51.063, -1.319],
27+
zoom: 8,
28+
maxBounds: [
29+
[49.528423, -10.76418],
30+
[61.331151, 1.9134116]
31+
],
32+
attributionControl: false
33+
}
34+
35+
const map = L.map('map', mapOptions)
36+
37+
// Load and display ZXY tile layer on the map.
38+
L.tileLayer('https://api.os.uk/maps/raster/v1/zxy/Light_3857/{z}/{x}/{y}.png?key=' + apiKey, {
39+
attribution: `Contains OS data © Crown copyright and database rights ${new Date().getFullYear()}`,
40+
maxZoom: 20
41+
}).addTo(map)
42+
43+
// window.GOVUK.icbGeojson and similar are declared in data files in views/missions/data
44+
45+
// Takes the icbGeoJSON and creates an object that looks like
46+
// {E1234567: "A UK Region", E1234568: "Another UK Region"} etc.
47+
// E numbers are region codes.
48+
// These are the more specific clickable UK 'regions' on the map
49+
const icbLookup = window.GOVUK.icbGeojson.features.reduce((obj, v) => ({ ...obj, [v.properties.ICB23CD]: v.properties.ICB23NM }), {})
50+
51+
// Creates an object with large UK regions e.g. { E12345678: "South East", }
52+
const nhsLookup = window.GOVUK.icbGeojson.features.reduce((obj, v) => ({ ...obj, [v.properties.NHSER22CD]: v.properties.NHSER22NM }), {})
53+
54+
// Colours for each large UK region
55+
const colorLookup = {
56+
E40000003: '#FF1F5B',
57+
E40000005: '#00CD6C',
58+
E40000006: '#009ADE',
59+
E40000007: '#AF58BA',
60+
E40000010: '#FFC61E',
61+
E40000011: '#F28522',
62+
E40000012: '#A0B1BA'
63+
}
64+
65+
// Add the DHSC Community Diagnostic Centre (CDC) locations.
66+
// AKA the clickable black circles on the map
67+
map.createPane('cdc')
68+
map.getPane('cdc').style.zIndex = 650
69+
70+
const cdcCustomOverlay = L.geoJSON(window.GOVUK.cdcGeojson, {
71+
onEachFeature: bindPopup,
72+
pointToLayer: function (feature, latlng) {
73+
return L.circleMarker(latlng)
74+
},
75+
style: function (feature) {
76+
return {
77+
color: '#fff',
78+
fillColor: '#000',
79+
fillOpacity: 1,
80+
radius: 5,
81+
weight: 2,
82+
pane: 'cdc'
83+
}
84+
}
85+
}).addTo(map)
86+
const cdcOverlay = cdcCustomOverlay
87+
88+
// Add the DHSC surgical hub locations.
89+
// AKA the clickable white circles on the map
90+
map.createPane('hub')
91+
map.getPane('hub').style.zIndex = 651
92+
93+
const hubCustomOverlay = L.geoJSON(window.GOVUK.hubGeojson, {
94+
onEachFeature: bindPopup,
95+
pointToLayer: function (feature, latlng) {
96+
return L.circleMarker(latlng)
97+
},
98+
style: function (feature) {
99+
return {
100+
color: '#000',
101+
fillColor: '#fff',
102+
fillOpacity: 1,
103+
radius: 4,
104+
weight: 1,
105+
pane: 'hub'
106+
}
107+
}
108+
}).addTo(map)
109+
const hubOverlay = hubCustomOverlay
110+
111+
// Generate CDC and surgical hub counts per ICB boundaries.
112+
const icbCdcCount = cdcOverlay.toGeoJSON().features.reduce(function (obj, v) {
113+
obj[v.properties.ICS_ICB] = (obj[v.properties.ICS_ICB] || 0) + 1
114+
return obj
115+
}, {})
116+
117+
const icbCdcIsOpenCount = cdcOverlay.toGeoJSON().features
118+
.filter((key, index) => key.properties.isOpen === 'Yes')
119+
.reduce((obj, v) => {
120+
obj[v.properties.icbCode] = (obj[v.properties.icbCode] || 0) + 1
121+
return obj
122+
}, {})
123+
124+
const icbCdcIsFullOpenCount = cdcOverlay.toGeoJSON().features
125+
.filter((key, index) => key.properties.isOpen12_7 === 'Yes')
126+
.reduce((obj, v) => {
127+
obj[v.properties.icbCode] = (obj[v.properties.icbCode] || 0) + 1
128+
return obj
129+
}, {})
130+
131+
const icbHubCount = hubOverlay.toGeoJSON().features.reduce(function (obj, v) {
132+
obj[v.properties.ICS_ICB] = (obj[v.properties.ICS_ICB] || 0) + 1
133+
return obj
134+
}, {})
135+
136+
const icbHubIsOpenCount = hubOverlay.toGeoJSON().features
137+
.filter((key, index) => key.properties.isOpen === 'Yes')
138+
.reduce((obj, v) => {
139+
obj[v.properties.icbCode] = (obj[v.properties.icbCode] || 0) + 1
140+
return obj
141+
}, {})
142+
143+
window.GOVUK.icbGeojson.features.forEach((element) => {
144+
element.properties.cdcCount = icbCdcCount[element.properties.ICB23CD] || 0
145+
element.properties.cdcOpen = icbCdcIsOpenCount[element.properties.ICB23CD] || 0
146+
element.properties.cdcFullOpen = icbCdcIsFullOpenCount[element.properties.ICB23CD] || 0
147+
element.properties.hubCount = icbHubCount[element.properties.ICB23CD] || 0
148+
element.properties.hubOpen = icbHubIsOpenCount[element.properties.ICB23CD] || 0
149+
})
150+
151+
// Add the Integrated Care Board (IBC) boundaries. (i.e. the UK sub-regions)
152+
map.createPane('icb')
153+
map.getPane('icb').style.zIndex = 450
154+
155+
const icb = L.geoJson(window.GOVUK.icbGeojson, {
156+
onEachFeature: bindPopup,
157+
style: function (feature) {
158+
return {
159+
color: '#fff',
160+
fillColor: colorLookup[feature.properties.NHSER22CD],
161+
fillOpacity: 0.5,
162+
weight: 1,
163+
pane: 'icb'
164+
}
165+
}
166+
}).addTo(map)
167+
168+
// Rough code to demonstrate filters.
169+
170+
const layers = [icb, hubOverlay, cdcOverlay]
171+
for (let i = 0; i < layers.length; i++) {
172+
const checkbox = document.querySelector(`#checkbox${i + 1}`)
173+
const mapLayer = layers[i]
174+
checkbox.addEventListener('click', function () {
175+
if (map.hasLayer(mapLayer)) {
176+
map.removeLayer(mapLayer)
177+
} else {
178+
map.addLayer(mapLayer)
179+
}
180+
})
181+
}
182+
183+
// Binds a popup to the layer with the passed content and sets up the necessary event listeners.
184+
// I.e. creates the popups that appear when you click on a circle or click on a region
185+
function bindPopup (feature, layer) {
186+
let properties = layer.feature.properties
187+
properties = (({ ICB23CD, NHSER22CD, ...o }) => o)(properties)
188+
189+
const propLookup = {
190+
nhsCode: 'nhsName',
191+
icbCode: 'icbName'
192+
}
193+
194+
const table = document.createElement('table')
195+
table.id = 'popup-table'
196+
197+
const popupHeading = document.createElement('h1')
198+
popupHeading.classList.add('govuk-heading')
199+
popupHeading.classList.add('govuk-heading-m')
200+
let popupSubheading = ''
201+
202+
// When you click on a circle, this popup will show up.
203+
if (layer.feature.geometry.type === 'Point') {
204+
popupHeading.innerText = properties.name
205+
let tableRow
206+
for (const i in properties) {
207+
if (i !== 'name') {
208+
const property = /E[\d]+/.test(properties[i]) ? i === 'nhsCode' ? nhsLookup[properties[i]] : icbLookup[properties[i]] : properties[i]
209+
tableRow = document.createElement('tr')
210+
const tableDataLabel = document.createElement('td')
211+
tableDataLabel.innerText = propLookup[i] || i
212+
const tableDataValue = document.createElement('td')
213+
tableDataValue.innerText = property
214+
215+
tableRow.appendChild(tableDataLabel)
216+
tableRow.appendChild(tableDataValue)
217+
table.appendChild(tableRow)
218+
}
219+
}
220+
} else {
221+
// When you click on a region, this popup will show up.
222+
popupHeading.innerText = `${properties.ICB23NM} Integrated Care Board`
223+
popupSubheading = document.createElement('h2')
224+
popupSubheading.classList.add('govuk-heading')
225+
popupSubheading.classList.add('govuk-heading-s')
226+
popupSubheading.innerText = `${properties.NHSER22NM} NHS Region`
227+
228+
const tableHeadingsRow = document.createElement('tr')
229+
const headings = ['&nbsp;', 'Total', 'Open', 'Open 12/7']
230+
231+
for (const heading of headings) {
232+
const tableHeading = document.createElement('th')
233+
tableHeading.innerHTML = heading
234+
tableHeadingsRow.appendChild(tableHeading)
235+
}
236+
237+
const tableCdcRow = document.createElement('tr')
238+
const tableCdcData = ['CDCs', properties.cdcCount, properties.cdcOpen, properties.cdcFullOpen]
239+
for (const cdcData of tableCdcData) {
240+
const tableData = document.createElement('td')
241+
tableData.innerText = cdcData
242+
tableCdcRow.appendChild(tableData)
243+
}
244+
245+
const tableHubRow = document.createElement('tr')
246+
const tableHubData = ['Surgical Hubs', properties.hubCount, properties.hubOpen, 'n/a']
247+
for (const hubData of tableHubData) {
248+
const tableData = document.createElement('td')
249+
tableData.innerText = hubData
250+
tableHubRow.appendChild(tableData)
251+
}
252+
253+
table.appendChild(tableHeadingsRow)
254+
table.appendChild(tableCdcRow)
255+
table.appendChild(tableHubRow)
256+
}
257+
258+
// Add the generated DOM elements above to a container and append it to the popup
259+
const tableContainer = document.createElement('div')
260+
tableContainer.appendChild(popupHeading)
261+
262+
if (popupSubheading) {
263+
tableContainer.appendChild(popupSubheading)
264+
}
265+
266+
tableContainer.appendChild(table)
267+
268+
const popup = layer.bindPopup(tableContainer)
269+
const onStyle = layer.feature.geometry.type === 'Point' ? { radius: Math.ceil(layer.options.radius * 1.2) } : { fillOpacity: layer.options.fillOpacity + 0.3 }
270+
const offStyle = layer.feature.geometry.type === 'Point' ? { radius: layer.options.radius } : { fillOpacity: layer.options.fillOpacity }
271+
popup.on('popupopen', (e) => { e.target.setStyle(onStyle) })
272+
popup.on('popupclose', (e) => { e.target.setStyle(offStyle) })
273+
}
274+
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* global os */
2+
// os-api-branding.js v0.3.1
3+
4+
var scriptTag = document.currentScript
5+
window.os = window.os || {}
6+
7+
os.Branding = {
8+
/**
9+
* Default configuration options.
10+
*/
11+
options: {
12+
div: 'map',
13+
logo: 'os-logo-maps', // os-logo-maps | os-logo-maps-white
14+
statement: 'Contains OS data &copy; Crown copyright and database rights YYYY',
15+
prefix: '',
16+
suffix: ''
17+
},
18+
19+
/**
20+
* Add the API logo and copyright statement.
21+
*/
22+
init: function (obj) {
23+
this.options.div = scriptTag.getAttribute('data-div') || this.options.div
24+
this.options.logo = scriptTag.getAttribute('data-logo') || this.options.logo
25+
this.options.statement = scriptTag.getAttribute('data-statement') || this.options.statement
26+
this.options.prefix = scriptTag.getAttribute('data-prefix') || this.options.prefix
27+
this.options.suffix = scriptTag.getAttribute('data-suffix') || this.options.suffix
28+
29+
obj = (typeof obj !== 'undefined') ? obj : {}
30+
Object.assign(this.options, obj)
31+
32+
var elem = document.getElementById(this.options.div)
33+
34+
if (!elem) return
35+
36+
var logoClassName = 'os-api-branding logo'
37+
if (this.options.logo === 'os-logo-maps-white') {
38+
logoClassName = 'os-api-branding logo white'
39+
}
40+
41+
var copyrightStatement = this.options.statement
42+
copyrightStatement = copyrightStatement.replace('YYYY', new Date().getFullYear())
43+
44+
if (this.options.prefix !== '') {
45+
copyrightStatement = this.options.prefix + '<span>|</span>' + copyrightStatement
46+
}
47+
48+
if (this.options.suffix !== '') {
49+
copyrightStatement = copyrightStatement + '<span>|</span>' + this.options.suffix
50+
}
51+
52+
document.querySelectorAll('#' + this.options.div + ' .os-api-branding').forEach(el => el.remove())
53+
54+
// Append the API logo.
55+
var div1 = document.createElement('div')
56+
div1.className = logoClassName
57+
elem.appendChild(div1)
58+
59+
// Append the copyright statement.
60+
var div2 = document.createElement('div')
61+
div2.className = 'os-api-branding copyright'
62+
div2.innerHTML = copyrightStatement
63+
elem.appendChild(div2)
64+
}
65+
}
66+
67+
os.Branding.init()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
class LandingPageController < ContentItemsController
22
slimmer_template "gem_layout_full_width"
3+
4+
content_security_policy do |policy|
5+
# The map block makes use of the OS api and inline styles
6+
policy.img_src(*policy.img_src, "https://api.os.uk", :data)
7+
end
38
end
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
<%
22
add_view_stylesheet("landing_page/map")
3+
add_view_stylesheet("landing_page/map/leaflet")
4+
add_view_stylesheet("landing_page/map/os-api-branding")
35
%>
6+
<% content_for :extra_javascript do %>
7+
<%= javascript_include_tag "views/landing_page/map/os-api-branding.js", integrity: false %>
8+
<%= javascript_include_tag "views/landing_page/map/main.js", integrity: false, type: "module" %>
9+
<% end %>
410

5-
map
11+
<div id="api-warning"></div>
12+
13+
<input type="checkbox" name="checkbox1" id="checkbox1" checked> Filter One<br/>
14+
<input type="checkbox" name="checkbox1" id="checkbox2" checked> Filter Two<br/>
15+
<input type="checkbox" name="checkbox1" id="checkbox3" checked> Filter Three <br/>
16+
17+
<div class="map" id="map" data-api-key="<%= ENV["OS_MAPS_API_KEY"] %>"></div>

0 commit comments

Comments
 (0)