if (!Object.keys) { Object.keys = function(obj) { var result = []; for (var prop in obj) { if (Object.prototype.hasOwnProperty.call(obj, prop)) { result.push(prop); } } return result; }; } function debounce(fn, delay) { var timeout; return function() { var that = this, args = arguments; var debounced = function() { timeout = null; fn.apply(that, args); }; clearTimeout(timeout); timeout = setTimeout(debounced, delay); }; } var fixmystreet = fixmystreet || {}; fixmystreet.utils = fixmystreet.utils || {}; $.extend(fixmystreet.utils, { array_to_csv_line: function(arr) { var out = [], s; for (var i=0; i= size_normal) { return window.selected_problem_id ? 'small' : 'normal'; } else if (zoom >= size_small) { return window.selected_problem_id ? 'mini' : 'small'; } else { return 'mini'; } }, selected_marker_size: function() { var zoom = fixmystreet.map.getZoom() + fixmystreet.zoomOffset; var size_normal = fixmystreet.maps.zoom_for_normal_size || 15; var size_small = fixmystreet.maps.zoom_for_small_size || 13; if (zoom >= size_normal) { return 'big'; } else if (zoom >= size_small) { return 'normal'; } else { return 'small'; } }, // Handle a single report pin being moved by dragging it on the map. // pin_moved_callback is called with a new EPSG:4326 OpenLayers.LonLat if // the user drags the pin and confirms its new location. admin_drag: function(pin_moved_callback, confirm_change) { if (fixmystreet.maps.admin_drag_control) { return; } confirm_change = confirm_change || false; var original_lonlat; var drag = fixmystreet.maps.admin_drag_control = new OpenLayers.Control.DragFeatureFMS( fixmystreet.markers, { onStart: function(feature, e) { // Keep track of where the feature started, so we can put it // back if the user cancels the operation. original_lonlat = new OpenLayers.LonLat(feature.geometry.x, feature.geometry.y); }, onComplete: function(feature, e) { var lonlat = feature.geometry.clone(); lonlat.transform( fixmystreet.map.getProjectionObject(), new OpenLayers.Projection("EPSG:4326") ); if ((confirm_change && window.confirm(translation_strings.correct_position)) || !confirm_change) { // Let the callback know about the newly confirmed position pin_moved_callback(lonlat); } else { // Put it back fixmystreet.markers.features[0].move(original_lonlat); } } } ); // Allow handled feature click propagation to other click handlers drag.handlers.feature.stopClick = false; fixmystreet.map.addControl( drag ); drag.activate(); }, // `markers.redraw()` in markers_highlight will trigger an // `overFeature` event if the mouse cursor is still over the same // marker on the map, which would then run markers_highlight // again, causing an infinite flicker while the cursor remains over // the same marker. We really only want to redraw the markers when // the cursor moves from one marker to another (ie: when there is an // overFeature followed by an outFeature followed by an overFeature). // Therefore, we keep track of the previous event in // fixmystreet.latest_map_hover_event and only call markers_highlight // if we know the previous event was different to the current one. // (See the `overFeature` and `outFeature` callbacks inside of // fixmystreet.select_feature). markers_highlight: function(problem_id) { if (!fixmystreet.markers) { return; } for (var i = 0; i < fixmystreet.markers.features.length; i++) { if (typeof problem_id == 'undefined') { // There is no highlighted marker, so unfade this marker fixmystreet.markers.features[i].attributes.faded = 0; } else if (problem_id == fixmystreet.markers.features[i].attributes.id) { // This is the highlighted marker, unfade it fixmystreet.markers.features[i].attributes.faded = 0; } else { // This is not the hightlighted marker, fade it fixmystreet.markers.features[i].attributes.faded = 1; } } fixmystreet.markers.redraw(); }, /* Keep track of how many things are loading simultaneously, and only hide * the loading spinner when everything has finished. * This allows multiple layers to be loading at once without each layer * having to keep track of the others or be responsible for manipulating * the spinner in the DOM. */ loading_spinner: { count: {}, show: function() { fixmystreet.maps.loading_spinner.count[this.id] = 1; if (Object.keys(fixmystreet.maps.loading_spinner.count).length) { // Show the loading indicator over the map $('#loading-indicator').removeClass('hidden'); $('#loading-indicator').attr('aria-hidden', false); } }, hide: function() { delete fixmystreet.maps.loading_spinner.count[this.id]; if (!Object.keys(fixmystreet.maps.loading_spinner.count).length) { // Remove loading indicator $('#loading-indicator').addClass('hidden'); $('#loading-indicator').attr('aria-hidden', true); } } }, get_map_state: function() { var centre = fixmystreet.map.getCenter(); return { zoom: fixmystreet.map.getZoom(), lat: centre.lat, lon: centre.lon, }; }, set_map_state: function(state) { fixmystreet.map.setCenter( new OpenLayers.LonLat( state.lon, state.lat ), state.zoom ); }, setup_geolocation: function() { if (!OpenLayers.Control.Geolocate || !fixmystreet.map || !fixmystreet.utils || !fixmystreet.utils.parse_query_string || fixmystreet.utils.parse_query_string().geolocate !== '1' ) { return; } var layer; function createCircleOfUncertainty(e) { var loc = new OpenLayers.Geometry.Point(e.point.x, e.point.y); return new OpenLayers.Feature.Vector( OpenLayers.Geometry.Polygon.createRegularPolygon( loc, e.position.coords.accuracy, 40, 0 ), {}, { fillColor: '#0074FF', fillOpacity: 0.3, strokeWidth: 0 } ); } function addGeolocationLayer(e) { layer = new OpenLayers.Layer.Vector('Geolocation'); fixmystreet.map.addLayer(layer); layer.setZIndex(fixmystreet.map.getLayersByName("Pins")[0].getZIndex() - 1); var marker = new OpenLayers.Feature.Vector( new OpenLayers.Geometry.Point(e.point.x, e.point.y), { marker: true }, { graphicName: 'circle', strokeColor: '#fff', strokeWidth: 4, fillColor: '#0074FF', fillOpacity: 1, pointRadius: 10 } ); layer.addFeatures([ createCircleOfUncertainty(e), marker ]); } function updateGeolocationMarker(e) { if (!layer) { addGeolocationLayer(e); } else { // Reuse the existing circle marker so its DOM element (and // hopefully CSS animation) is preserved. var marker = layer.getFeaturesByAttribute('marker', true)[0]; // Can't reuse the background circle feature as there seems to // be no easy way to replace its geometry with a new // circle sized according to this location update's accuracy. // Instead recreate the feature from scratch. var uncertainty = createCircleOfUncertainty(e); // Because we're replacing the accuracy circle, it needs to be // rendered underneath the location marker. In order to do this // we have to remove all features and re-add, as simply removing // and re-adding one feature will always render it on top of others. layer.removeAllFeatures(); layer.addFeatures([ uncertainty, marker ]); // NB The above still breaks CSS animation because the marker // was removed from the DOM and re-added. We could leave the // marker alone and just remove the uncertainty circle // feature, re-add it as a new feature and then manually shift // its position in the DOM by getting its element's ID from // uncertainty.geometry.id and moving it before the // element. // Don't forget to update the position of the GPS marker. marker.move(new OpenLayers.LonLat(e.point.x, e.point.y)); } } var control = new OpenLayers.Control.Geolocate({ bind: false, // Don't want the map to pan to each location watch: true, enableHighAccuracy: true }); control.events.register("locationupdated", null, updateGeolocationMarker); fixmystreet.map.addControl(control); control.activate(); } }); /* Make sure pins aren't going to reload just because we're zooming out, * we already have the pins when the page loaded */ function zoomToBounds(bounds) { if (!bounds || !fixmystreet.markers.strategies) { return; } var strategy = fixmystreet.markers.strategies[0]; strategy.deactivate(); var center = bounds.getCenterLonLat(); var z = fixmystreet.map.getZoomForExtent(bounds); fixmystreet.map.setCenter(center, z); // Reactivate the strategy and make it think it's done an update strategy.activate(); if (strategy instanceof OpenLayers.Strategy.BBOX) { strategy.calculateBounds(); strategy.resolution = fixmystreet.map.getResolution(); } } function sidebar_highlight(problem_id) { if (typeof problem_id !== 'undefined') { var $li = $('[data-report-id="' + problem_id + '"]'); $li.addClass('hovered'); } else { $('.item-list .hovered').removeClass('hovered'); } } function marker_click(problem_id, evt) { var $a = $('.item-list a[href$="/' + problem_id + '"]'); if (!$a[0]) { return; } // clickFeature operates on touchstart, we do not want the map click taking place on touchend! if (fixmystreet.maps.click_control) { fixmystreet.maps.click_control.deactivate(); } // All of this, just so that ctrl/cmd-click on a pin works?! var event; if (typeof window.MouseEvent === 'function') { event = new MouseEvent('click', evt); $a[0].dispatchEvent(event); } else if (document.createEvent) { event = document.createEvent("MouseEvents"); event.initMouseEvent( 'click', true, true, window, 1, 0, 0, 0, 0, evt.ctrlKey, evt.altKey, evt.shiftKey, evt.metaKey, 0, null); $a[0].dispatchEvent(event); } else if (document.createEventObject) { event = document.createEventObject(); event.metaKey = evt.metaKey; event.ctrlKey = evt.ctrlKey; if (event.metaKey === undefined) { event.metaKey = event.ctrlKey; } $a[0].fireEvent("onclick", event); } else { $a[0].click(); } } var categories_or_status_changed = debounce(function() { // If the category or status has changed we need to re-fetch map markers fixmystreet.markers.refresh({force: true}); }, 1000); function replace_query_parameter(qs, id, key) { var value, $el = $('#' + id); if (!$el[0]) { return; } if ( $el[0].type === 'checkbox' ) { value = $el[0].checked ? '1' : ''; if (value) { qs[key] = value; } else { delete qs[key]; } } else { value = $el.val(); if (value) { qs[key] = (typeof value === 'string') ? value : fixmystreet.utils.array_to_csv_line(value); } else { delete qs[key]; } } return value; } function update_url(qs) { var new_url; if ($.isEmptyObject(qs)) { new_url = location.href.replace(location.search, ""); } else if (location.search) { new_url = location.href.replace(location.search, '?' + $.param(qs)); } else { new_url = location.href + '?' + $.param(qs); } return new_url; } function update_history(qs, data) { var new_url = update_url(qs); history.pushState(data, null, new_url); // Ensure the permalink control is updated when the filters change var permalink_controls = fixmystreet.map.getControlsByClass(/Permalink/); if (permalink_controls.length) { permalink_controls[0].updateLink(); } } function page_changed_history() { if (!('pushState' in history)) { return; } var qs = fixmystreet.utils.parse_query_string(); var show_old_reports = replace_query_parameter(qs, 'show_old_reports', 'show_old_reports'); var page = $('.pagination:first').data('page'); if (page > 1) { qs.p = page; } else { delete qs.p; } update_history(qs, { page_change: { 'page': page, 'show_old_reports': show_old_reports } }); } function categories_or_status_changed_history() { if (!('pushState' in history)) { return; } var qs = fixmystreet.utils.parse_query_string(); var filter_categories = replace_query_parameter(qs, 'filter_categories', 'filter_category'); var filter_statuses = replace_query_parameter(qs, 'statuses', 'status'); var sort_key = replace_query_parameter(qs, 'sort', 'sort'); var show_old_reports = replace_query_parameter(qs, 'show_old_reports', 'show_old_reports'); delete qs.p; update_history(qs, { filter_change: { 'filter_categories': filter_categories, 'statuses': filter_statuses, 'sort': sort_key, 'show_old_reports': show_old_reports } }); } function setup_inspector_marker_drag() { // On the 'inspect report' page the pin is draggable, so we need to // update the easting/northing fields when it's dragged. if (!$('form#report_inspect_form').length) { // Not actually on the inspect report page return; } fixmystreet.maps.admin_drag(function(geom) { var lonlat = new OpenLayers.LonLat(geom.x, geom.y); fixmystreet.maps.update_pin_input_fields(lonlat); }, false); } function onload() { if ( fixmystreet.area.length ) { var extent = new OpenLayers.Bounds(); var lr = new OpenLayers.Geometry.LinearRing([ new OpenLayers.Geometry.Point(20E6,20E6), new OpenLayers.Geometry.Point(10E6,20E6), new OpenLayers.Geometry.Point(0,20E6), new OpenLayers.Geometry.Point(-10E6,20E6), new OpenLayers.Geometry.Point(-20E6,20E6), new OpenLayers.Geometry.Point(-20E6,0), new OpenLayers.Geometry.Point(-20E6,-20E6), new OpenLayers.Geometry.Point(-10E6,-20E6), new OpenLayers.Geometry.Point(0,-20E6), new OpenLayers.Geometry.Point(10E6,-20E6), new OpenLayers.Geometry.Point(20E6,-20E6), new OpenLayers.Geometry.Point(20E6,0) ]); var loaded = 0; var new_geometry = new OpenLayers.Geometry.Polygon(lr); var style_area = function() { loaded++; var style = this.styleMap.styles['default']; if ( fixmystreet.area_format ) { style.defaultStyle = fixmystreet.area_format; } else { $.extend(style.defaultStyle, { fillColor: 'black', strokeColor: 'black' }); } if (!this.features.length) { return; } var geometry = this.features[0].geometry; if (geometry.CLASS_NAME == 'OpenLayers.Geometry.Collection' || geometry.CLASS_NAME == 'OpenLayers.Geometry.MultiPolygon') { $.each(geometry.components, function(i, polygon) { new_geometry.addComponents(polygon.components); extent.extend(polygon.getBounds()); }); } else if (geometry.CLASS_NAME == 'OpenLayers.Geometry.Polygon') { new_geometry.addComponents(geometry.components); extent.extend(this.getDataExtent()); } if (loaded == fixmystreet.area.length) { var f = this.features[0].clone(); f.geometry = new_geometry; this.removeAllFeatures(); this.addFeatures([f]); // Look at original href here to know if location was present at load. // If it was, we don't want to zoom out to the bounds of the area. var qs = OpenLayers.Util.getParameters(fixmystreet.original.href); if (!qs.bbox && !qs.lat && !qs.lon) { zoomToBounds(extent); } } else { fixmystreet.map.removeLayer(this); } }; for (var i=0; i