var fixmystreet = fixmystreet || {}; (function() { fixmystreet.maps = { // This function might be passed either an OpenLayers.LonLat (so has // lon and lat), or an OpenLayers.Geometry.Point (so has x and y). update_pin: function(lonlat) { var transformedLonlat = lonlat.clone().transform( fixmystreet.map.getProjectionObject(), new OpenLayers.Projection("EPSG:4326") ); var lat = transformedLonlat.lat || transformedLonlat.y; var lon = transformedLonlat.lon || transformedLonlat.x; document.getElementById('fixmystreet.latitude').value = lat; document.getElementById('fixmystreet.longitude').value = lon; return { 'url': { 'lon': lon, 'lat': lat }, 'state': { 'lon': lonlat.lon, 'lat': lonlat.lat } }; }, display_around: function() { // Required after changing the size of the map element fixmystreet.map.updateSize(); // Dragging the map should fetch new local reports from server if (fixmystreet.bbox_strategy) { fixmystreet.bbox_strategy.activate(); } // Should not be able to drag normal pins!! drag.deactivate(); // Force a redraw to return (de)selected marker to normal size // Redraw for all pages, kick off a refresh too for around // TODO Put 'new report' pin in different layer to simplify this and elsewhere fixmystreet.maps.markers_resize(); fixmystreet.markers.refresh({force: true}); }, begin_report: function(lonlat) { if (typeof lonlat.clone !== 'function') { lonlat = new OpenLayers.LonLat(lonlat.lon, lonlat.lat); } if (fixmystreet.page == 'new') { /* Already have a pin */ fixmystreet.markers.features[0].move(lonlat); } else { var markers = fixmystreet.maps.markers_list( [ [ lonlat.lat, lonlat.lon, 'green' ] ], false ); fixmystreet.bbox_strategy.deactivate(); fixmystreet.markers.removeAllFeatures(); fixmystreet.markers.addFeatures( markers ); drag.activate(); } // check to see if markers are visible. We click the // link so that it updates the text in case they go // back if ( ! fixmystreet.markers.getVisibility() ) { $('#hide_pins_link').click(); } return lonlat; }, markers_list: function(pins, transform) { var markers = []; var size = fixmystreet.maps.marker_size_for_zoom( fixmystreet.map.getZoom() + fixmystreet.zoomOffset ); var selected_size = fixmystreet.maps.selected_marker_size_for_zoom( fixmystreet.map.getZoom() + fixmystreet.zoomOffset ); for (var i=0; i= 15) { return window.selected_problem_id ? 'small' : 'normal'; } else if (zoom >= 13) { return window.selected_problem_id ? 'mini' : 'small'; } else { return 'mini'; } }, selected_marker_size_for_zoom: function(zoom) { if (zoom >= 15) { return 'big'; } else if (zoom >= 13) { 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) { confirm_change = confirm_change || false; var original_lonlat; var drag = new OpenLayers.Control.DragFeature( 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); } } } ); fixmystreet.map.addControl( drag ); drag.activate(); } }; var drag = { activate: function() { this._drag = new OpenLayers.Control.DragFeature( fixmystreet.markers, { onComplete: function(feature, e) { fixmystreet.update_pin( feature.geometry ); } } ); fixmystreet.map.addControl( this._drag ); this._drag.activate(); }, deactivate: function() { this._drag && this._drag.deactivate(); } }; function zoomToBounds(bounds) { if (!bounds) { return; } var center = bounds.getCenterLonLat(); var z = fixmystreet.map.getZoomForExtent(bounds); if ( z < 13 && $('html').hasClass('mobile') ) { z = 13; } fixmystreet.map.setCenter(center, z); } // `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). function markers_highlight(problem_id) { 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(); } function sidebar_highlight(problem_id) { if (typeof problem_id !== 'undefined') { var $a = $('.item-list--reports a[href$="/' + problem_id + '"]'); $a.parent().addClass('hovered'); } else { $('.item-list--reports .hovered').removeClass('hovered'); } } function marker_click(problem_id, evt) { var $a = $('.item-list--reports a[href$="/' + problem_id + '"]'); if (!$a[0]) { return; } // All of this, just so that ctrl/cmd-click on a pin works?! var event; if (window.MouseEvent) { 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 (e.metaKey === undefined) { e.metaKey = e.ctrlKey; } $a[0].fireEvent("onclick", event); } else { $a[0].click(); } } function categories_or_status_changed() { // If the category or status has changed we need to re-fetch map markers fixmystreet.markers.refresh({force: true}); } function parse_query_string() { var qs = {}; location.search.substring(1).split('&').forEach(function(i) { var s = i.split('='), k = s[0], v = s[1] && decodeURIComponent(s[1].replace(/\+/g, ' ')); qs[k] = v; }); return qs; } function replace_query_parameter(qs, id, key) { var value = $('#' + id).val(); value ? qs[key] = value.join(',') : delete qs[key]; return value; } function categories_or_status_changed_history() { if (!('pushState' in history)) { return; } var qs = parse_query_string(); var filter_categories = replace_query_parameter(qs, 'filter_categories', 'filter_category'); var filter_statuses = replace_query_parameter(qs, 'statuses', 'status'); var new_url = location.href.replace(location.search, '?' + $.param(qs)); history.pushState({ filter_change: { 'filter_categories': filter_categories, 'statuses': filter_statuses } }, null, new_url); } 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(lonlat) { var bng = lonlat.clone().transform( new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:27700") // TODO: Handle other projections ); $("#problem_northing").text(bng.y.toFixed(1)); $("#problem_easting").text(bng.x.toFixed(1)); $("form#report_inspect_form input[name=latitude]").val(lonlat.y); $("form#report_inspect_form input[name=longitude]").val(lonlat.x); }, false); } function onload() { if ( fixmystreet.area.length ) { for (var i=0; i -1) { btn.map = this.map; OpenLayers.Event.observe(btn, "mousedown", OpenLayers.Function.bindAsEventListener(this.buttonDown, btn)); var slideFactorPixels = this.slideFactor; btn.getSlideFactor = function() { return slideFactorPixels; }; } this.buttons.push(btn); return btn; }, moveTo: function(){}, draw: function(px) { // A customised version of .draw() that doesn't specify // and dimensions/positions for the buttons, since we // size and position them all using CSS. OpenLayers.Control.prototype.draw.apply(this, arguments); this.buttons = []; this._addButton("pan", "up"); this._addButton("pan", "left"); this._addButton("pan", "right"); this._addButton("pan", "down"); this._addButton("zoom", "in"); this._addButton("zoom", "out"); return this.div; } }); /* Overriding Permalink so that it can pass the correct zoom to OSM */ OpenLayers.Control.PermalinkFMS = OpenLayers.Class(OpenLayers.Control.Permalink, { _updateLink: function(alter_zoom) { var separator = this.anchor ? '#' : '?'; var href = this.base; if (href.indexOf(separator) != -1) { href = href.substring( 0, href.indexOf(separator) ); } var center = this.map.getCenter(); var zoom = this.map.getZoom(); if ( alter_zoom ) { zoom += fixmystreet.zoomOffset; } href += separator + OpenLayers.Util.getParameterString(this.createParams(center, zoom)); // Could use mlat/mlon here as well if we are on a page with a marker if (this.anchor && !this.element) { window.location.href = href; } else { this.element.href = href; } }, updateLink: function() { this._updateLink(0); } }); OpenLayers.Control.PermalinkFMSz = OpenLayers.Class(OpenLayers.Control.PermalinkFMS, { updateLink: function() { this._updateLink(1); } }); OpenLayers.Strategy.FixMyStreet = OpenLayers.Class(OpenLayers.Strategy.BBOX, { ratio: 1, // The transform in Strategy.BBOX's getMapBounds could mean you end up with // co-ordinates too precise, which could then cause the Strategy to think // it needs to update when it doesn't. So create a new bounds out of the // provided one to make sure it's passed through toFloat(). getMapBounds: function() { var bounds = OpenLayers.Strategy.BBOX.prototype.getMapBounds.apply(this); if (bounds) { bounds = new OpenLayers.Bounds(bounds.toArray()); } return bounds; }, // The above isn't enough, however, because Strategy.BBOX's getMapBounds // and calculateBounds work out the bounds in different ways, the former by // transforming the map's extent to the layer projection, the latter by // adding or subtracting from the centre. As we have a ratio of 1, rounding // errors can still occur. This override makes calculateBounds always equal // getMapBounds (so no movement means no update). calculateBounds: function(mapBounds) { if (!mapBounds) { mapBounds = this.getMapBounds(); } this.bounds = mapBounds; } }); /* Pan data request handler */ // This class is used to get a JSON object from /ajax that contains // pins for the map and HTML for the sidebar. It does a fetch whenever the map // is dragged (modulo a buffer extending outside the viewport). // This subclass is required so we can pass the 'filter_category' and 'status' query // params to /ajax if the user has filtered the map. OpenLayers.Protocol.FixMyStreet = OpenLayers.Class(OpenLayers.Protocol.HTTP, { read: function(options) { // Pass the values of the category and status fields as query params var filter_category = $("#filter_categories").val(); if (filter_category !== undefined) { options.params = options.params || {}; options.params.filter_category = filter_category; } var status = $("#statuses").val(); if (status !== undefined) { options.params = options.params || {}; options.params.status = status; } return OpenLayers.Protocol.HTTP.prototype.read.apply(this, [options]); }, CLASS_NAME: "OpenLayers.Protocol.FixMyStreet" }); /* Pan data handler */ OpenLayers.Format.FixMyStreet = OpenLayers.Class(OpenLayers.Format.JSON, { read: function(json, filter) { // Check we haven't received the data after the map has been clicked. if (fixmystreet.page == 'new') { // If we have, we want to do nothing, which means returning an // array of the back-projected version of the current pin var pin = fixmystreet.markers.features[0].clone(); pin.geometry.transform( fixmystreet.map.getProjectionObject(), new OpenLayers.Projection("EPSG:4326") ); return [ pin ]; } if (typeof json == 'string') { obj = OpenLayers.Format.JSON.prototype.read.apply(this, [json, filter]); } else { obj = json; } var current; if (typeof(obj.current) != 'undefined' && (current = document.getElementById('current'))) { current.innerHTML = obj.current; } return fixmystreet.maps.markers_list( obj.pins, false ); }, CLASS_NAME: "OpenLayers.Format.FixMyStreet" }); /* Click handler */ OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, { defaultHandlerOptions: { 'single': true, 'double': false, 'pixelTolerance': 4, 'stopSingle': false, 'stopDouble': false }, initialize: function(options) { this.handlerOptions = OpenLayers.Util.extend( {}, this.defaultHandlerOptions); OpenLayers.Control.prototype.initialize.apply( this, arguments ); this.handler = new OpenLayers.Handler.Click( this, { 'click': this.trigger }, this.handlerOptions); }, trigger: function(e) { // If we are looking at an individual report, and the report was // ajaxed into the DOM from the all reports page, then clicking // the map background should take us back to the all reports list. if ($('.js-back-to-report-list').length) { $('.js-back-to-report-list').trigger('click'); return true; } var lonlat = fixmystreet.map.getLonLatFromViewPortPx(e.xy); fixmystreet.display.begin_report(lonlat); if ( typeof ga !== 'undefined' && fixmystreet.cobrand == 'fixmystreet' ) { ga('send', 'pageview', { 'page': '/map_click' } ); } } });