// mapbuilder.js v0.2.2 var map, routeLayer, routeMarkerLayer, markerLayer; var styles = ''; function init() { var tileServiceUrl = 'https://api.os.uk/maps/raster/v1/zxy', nameServiceUrl = 'https://api.os.uk/search/names/v1'; // Setup the EPSG:27700 (British National Grid) projection. proj4.defs("EPSG:27700", "+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs"); ol.proj.proj4.register(proj4); var extent = [ -238375.0, 0.0, 900000.0, 1376256.0 ]; var tilegrid = new ol.tilegrid.TileGrid({ resolutions: [ 896.0, 448.0, 224.0, 112.0, 56.0, 28.0, 14.0, 7.0, 3.5, 1.75 ], origin: [ -238375.0, 1376256.0 ] }); var tileSource = new ol.source.XYZ({ url: tileServiceUrl + '/' + mapConfig.style + '_27700/{z}/{x}/{y}.png?key=' + apiKey, projection: 'EPSG:27700', tileGrid: tilegrid }); // Initialize the map object. map = new ol.Map({ layers: [ new ol.layer.Tile({ source: tileSource }) ], target: 'map', view: new ol.View({ projection: 'EPSG:27700', extent: extent, resolutions: tilegrid.getResolutions(), minZoom: 0, maxZoom: 9, center: mapConfig.center, zoom: mapConfig.zoom }) }); if( mapConfig.controls.coordinates ) { styles += `.custom-mouse-position { display:block; position:absolute; margin:auto 25%; width:50%; padding:10px 0; border:none; border-radius:0 0 3px 3px; text-align:center; color:#fff; background:#333; z-index:9999; } `; // Define the mouse position control. var mousePositionControl = new ol.control.MousePosition({ coordinateFormat: getCoordinateString, className: 'custom-mouse-position', undefinedHTML: '', }); map.controls.push(mousePositionControl); } if( mapConfig.controls.gazetteer ) { styles += `.ol-geocoder .gcd-gl-btn { background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNSIgaGVpZ2h0PSIxNSIgdmlld0JveD0iMCAwIDMwIDMwIj4KPHBhdGggZD0iTTIwLjE5NCwzLjQ2Yy00LjYxMy00LjYxMy0xMi4xMjEtNC42MTMtMTYuNzM0LDBjLTQuNjEyLDQuNjE0LTQuNjEyLDEyLjEyMSwwLDE2LjczNWM0LjEwOCw0LjEwNywxMC41MDYsNC41NDcsMTUuMTE2LDEuMzRjMC4wOTcsMC40NTksMC4zMTksMC44OTcsMC42NzYsMS4yNTRsNi43MTgsNi43MThjMC45NzksMC45NzcsMi41NjEsMC45NzcsMy41MzUsMGMwLjk3OC0wLjk3OCwwLjk3OC0yLjU2LDAtMy41MzVsLTYuNzE4LTYuNzJjLTAuMzU1LTAuMzU0LTAuNzk0LTAuNTc3LTEuMjUzLTAuNjc0QzI0Ljc0MywxMy45NjcsMjQuMzAzLDcuNTcsMjAuMTk0LDMuNDZ6TTE4LjA3MywxOC4wNzRjLTMuNDQ0LDMuNDQ0LTkuMDQ5LDMuNDQ0LTEyLjQ5MiwwYy0zLjQ0Mi0zLjQ0NC0zLjQ0Mi05LjA0OCwwLTEyLjQ5MmMzLjQ0My0zLjQ0Myw5LjA0OC0zLjQ0MywxMi40OTIsMEMyMS41MTcsOS4wMjYsMjEuNTE3LDE0LjYzLDE4LjA3MywxOC4wNzR6IiBmaWxsPSIjZmZmIi8+Cjwvc3ZnPg=="); } .ol-geocoder ul.gcd-gl-result { max-height:13.75em; } `; // Set the provider (passing any options that are required). var provider = osOpenNames({ url: nameServiceUrl + '/find', }); // Instantiate the Geocoder control. var geocoder = new Geocoder('nominatim', { provider: provider, autoComplete: true, autoCompleteMinLength: 3, placeholder: 'Search...', targetType: 'glass-button', lang: 'en', keepOpen: false, preventDefault: true }); map.addControl(geocoder); // Event handler which is triggered when an address is chosen. geocoder.on('addresschosen', function(evt) { if( evt.bbox ) map.getView().fit(evt.bbox, { duration: 500 }); else map.getView().animate({ zoom: 9, center: evt.coordinate }); }); } if( mapConfig.controls.overview ) { styles += `.ol-custom-overviewmap, .ol-custom-overviewmap.ol-uncollapsible { top:auto; bottom:24px; left:auto; right:8px; } `; // Define the overview map control. var overviewMapControl = new ol.control.OverviewMap({ className: 'ol-overviewmap ol-custom-overviewmap', collapsible: false, layers: [ new ol.layer.Tile({ source: tileSource }) ], view: new ol.View({ projection: 'EPSG:27700', extent: extent }) }); map.controls.push(overviewMapControl); } var styleSheet = document.createElement('style'); styleSheet.type = 'text/css'; styleSheet.innerText = styles; document.head.appendChild(styleSheet); if( typeof routeFeature !== 'undefined' ) { // Add a route to the map. var routeSourceObj = Array.isArray(routeFeature) ? { features: routeFeature } : { url: routeFeature, format: new ol.format.GPX() }; routeLayer = new ol.layer.Vector({ name: 'route', source: new ol.source.Vector(routeSourceObj), style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(0, 0, 205, 0.5)', width: 4 }) }) }); map.addLayer(routeLayer); // Add a route markers to the map. routeMarkerLayer = new ol.layer.Vector({ name: 'routeMarker', source: new ol.source.Vector({ features: [] }), style: routeMarkerStyle }); map.addLayer(routeMarkerLayer); if( Array.isArray(routeFeature) ) { addRouteMarkers(); } else { // Create an event listener as a handler for once all the data has been loaded // into the route layer. routeLayer.once("change", addRouteMarkers); } } if( typeof markerFeatures !== 'undefined' ) { // Add the cluster markers to the map. markerLayer = new ol.layer.Vector({ name: 'marker', source: new ol.source.Cluster({ // distance: 20, source: new ol.source.Vector({ features: markerFeatures }) }), style: markerStyle }); map.addLayer(markerLayer); } if( mapConfig.markers.interactive ) { // Define a new popup and add it as an overlay to the map. var popup = new Popup({ offset: [ 0, -24 ] }); map.addOverlay(popup); // Create a 'singleclick' event handler which displays a popup with some basic HTML content. map.on('singleclick', function(evt) { popup.hide(); var selectedFeatures = []; var feature = map.forEachFeatureAtPixel(evt.pixel, function(feature, layer) { var originalFeatures = feature.get('features'); if( layer.get('name') == 'marker' ) { if( originalFeatures.length == 1 ) { selectedFeatures.push(originalFeatures[0]); } else if( originalFeatures.length > 1 ) { var extent = new ol.extent.createEmpty(); for( var i = 0; i < originalFeatures.length; i++ ) { ol.extent.extend(extent, originalFeatures[i].getGeometry().getExtent()); } map.getView().fit(extent, { padding: [ 20, 20, 20, 20 ], duration: 200 }); } } }); if( selectedFeatures.length > 0 ) { var coords = selectedFeatures[0].get('geometry').getCoordinates(), content = selectedFeatures[0].get('content'); popup.show(coords, content); } }); // Use hasFeatureAtPixel() to indicate that the features are clickable by changing the // cursor style to 'pointer'. map.on('pointermove', function(evt) { if( evt.dragging ) return; var hit = map.hasFeatureAtPixel(evt.pixel, { layerFilter: function(layer) { return layer.get('name') === 'marker'; } }); map.getViewport().style.cursor = hit ? 'pointer' : ''; }); // Create a 'change:resolution' event handler which hides any popups when view resolution changes. map.getView().on('change:resolution', function(evt) { popup.hide(); }); } /** * Returns string with templated details for the input coordinate location. */ function getCoordinateString(coord) { var templateOSGB = 'Easting: {x} Northing: {y}'; var strOSGB = ol.coordinate.format(coord, templateOSGB, 0); var templateWGS84 = 'Longitude: {x} Latitude: {y}'; var strWGS84 = ol.coordinate.format(ol.proj.transform(coord, 'EPSG:27700', 'EPSG:4326'), templateWGS84, 6); var gridRef = toGridRef({ ea: coord[0], no: coord[1] }); var strGridRef = 'OS Grid Reference: ' + gridRef.text; return strOSGB + '
' + strWGS84 + '
' + strGridRef; } /** * Convert northing and easting to letter and number grid system. */ function toGridRef(coordinates) { var prefixes = new Array( new Array("SV","SW","SX","SY","SZ","TV","TW"), new Array("SQ","SR","SS","ST","SU","TQ","TR"), new Array("SL","SM","SN","SO","SP","TL","TM"), new Array("SF","SG","SH","SJ","SK","TF","TG"), new Array("SA","SB","SC","SD","SE","TA","TB"), new Array("NV","NW","NX","NY","NZ","OV","OW"), new Array("NQ","NR","NS","NT","NU","OQ","OR"), new Array("NL","NM","NN","NO","NP","OL","OM"), new Array("NF","NG","NH","NJ","NK","OF","OG"), new Array("NA","NB","NC","ND","NE","OA","OB"), new Array("HV","HW","HX","HY","HZ","JV","JW"), new Array("HQ","HR","HS","HT","HU","JQ","JR"), new Array("HL","HM","HN","HO","HP","JL","JM") ); var x = Math.floor(coordinates.ea / 100000); var y = Math.floor(coordinates.no / 100000); var prefix = prefixes[y][x]; var e = Math.round(coordinates.ea % 100000); var n = Math.round(coordinates.no % 100000); e = String(e).padStart(5, '0'); n = String(n).padStart(5, '0'); var text = prefix + ' ' + e + ' ' + n, html = prefix + ' ' + e + ' ' + n; return { text: text, html: html }; } /** * Custom provider for OS Names API. * Factory function which returns an object with the methods 'getParameters' * and 'handleResponse' called by the Geocoder. */ function osOpenNames(options) { var url = options.url; return { getParameters: function(opt) { return { url: url, params: { key: apiKey, query: opt.query, maxresults: 5 } }; }, handleResponse: function(data) { // On error show alert with returned error message. if( data.error ) { alert('Error: ' + data.error.message); return; } var response = []; if( data.header.totalresults > 0 ) { data.results.forEach(function(val, i) { var result = val.GAZETTEER_ENTRY; // Transform the returned coordinates into latitude and longitude. var coords = ol.proj.transform( [ result.GEOMETRY_X, result.GEOMETRY_Y ], 'EPSG:27700', 'EPSG:4326' ); // Generate the populated place string. var name = result.NAME1; if( result.NAME2 ) { name += '/'; name += result.NAME2; } name = name.toUpperCase(); var entity = [ name ]; if( result.POPULATED_PLACE ) { entity.push(result.POPULATED_PLACE); } if( result.DISTRICT_BOROUGH ) { entity.push(result.DISTRICT_BOROUGH); } if( result.COUNTY_UNITARY ) { entity.push(result.COUNTY_UNITARY); } entity.push(result.REGION); entity.push(result.COUNTRY); entity = entity.filter((item, index) => entity.indexOf(item) === index); response[i] = { lon: coords[0], lat: coords[1], address: { name: entity.join(', ') } }; // Add transformed bounding box value to the response. // Minimum bounding rectangle (MBR) is returned for all features except postcodes. if( result.MBR_XMIN && result.MBR_YMIN && result.MBR_XMAX && result.MBR_YMAX ) response[i].bbox = ol.proj.transformExtent( [ result.MBR_XMIN, result.MBR_YMIN, result.MBR_XMAX, result.MBR_YMAX ], 'EPSG:27700', 'EPSG:4326' ); }); } return response; } }; } /** * Add route markers. */ function addRouteMarkers() { // Get the geometry for the route. var geometry = routeLayer.getSource().getFeatures()[0].getGeometry(); // Extract the first and last coordinates for the route. var startCoord = geometry.getFirstCoordinate(), endCoord = geometry.getLastCoordinate(); // Create a marker features with for start and end of the route. var features = [ new ol.Feature({ icon: 'start', geometry: new ol.geom.Point(startCoord) }), new ol.Feature({ icon: 'end', geometry: new ol.geom.Point(endCoord) }) ]; // If the start and end coordinates are within 5m of each other - create a single marker // feature at the end of the route. if( new ol.geom.LineString([ startCoord, endCoord ]).getLength() <= 5 ) { features = [ new ol.Feature({ icon: 'startend', geometry: new ol.geom.Point(endCoord) }) ]; } // Add marker features to the route marker layer. routeMarkerLayer.getSource().addFeatures(features); } /** * Returns data-driven style object for the route markers. */ function routeMarkerStyle(feature) { // Define an SVG (sprite) object to be used for the route marker icon. var svg = ` `; var img = new Image(); img.src = 'data:image/svg+xml,' + escape(svg); // Define sub-rectangle offsets. var offsets = { 'start': [ 0, 0 ], 'end': [ 0, 48 ], 'startend': [ 0, 96 ] }; var style = new ol.style.Style({ image: new ol.style.Icon({ anchor: [ 0.2, 0.9 ], img: img, offset: offsets[feature.get('icon')], imgSize: [ 48, 48 ], scale: 0.75 }) }); return style; } /** * Returns data-driven style object for the marker clusters. */ function markerStyle(feature) { var color = feature.get('features')[0].get('color') || mapConfig.markers.color; // Define an SVG object to be used for the marker icon. var svg = ` `; var img = new Image(); img.src = 'data:image/svg+xml,' + escape(svg); var style = new ol.style.Style({ image: new ol.style.Icon({ anchor: [ 0.5, 1 ], img: img, imgSize: [ 48, 48 ], scale: 0.75 }), zIndex: 1 }); var size = feature.get('features').length; // Return a marker cluster if the number of features is greater than one. // Otherwise style the single feature using a standard marker icon. if( size > 1 ) { // Define the text string to show inside the marker cluster icon. var textString = (size < 10) ? size.toString() : (size >= 10 && size < 20) ? '10+' : (size >= 20 && size < 30) ? '20+' : (size >= 30 && size < 50) ? '30+' : '50+'; style = new ol.style.Style({ image: new ol.style.Circle({ radius: 15, fill: new ol.style.Fill({ color: mapConfig.markers.color, }) }), text: new ol.style.Text({ font: 'bold 12px sans-serif', text: textString, fill: new ol.style.Fill({ color: '#fff' }) }), zIndex: 2 }); } return style; } }