diff --git a/extension.json b/extension.json index d298bc4bb..480f2e5dc 100644 --- a/extension.json +++ b/extension.json @@ -248,7 +248,8 @@ "ext.maps.googlemaps3": { "scripts": [ "GoogleMaps/jquery.googlemap.js", - "GoogleMaps/ext.maps.googlemaps3.js" + "GoogleMaps/ext.maps.googlemaps3.js", + "GoogleMaps/mylocation.js" ], "messages": [ "maps-googlemaps3-incompatbrowser", @@ -256,6 +257,7 @@ "maps-searchmarkers-text", "maps-fullscreen-button", "maps-fullscreen-button-tooltip", + "maps-mylocation-button-tooltip", "maps-kml-parsing-failed" ], "targets": [ "desktop", "mobile" ] diff --git a/i18n/en.json b/i18n/en.json index fa7b345ea..dc434e968 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -81,6 +81,7 @@ "maps-displaymap-par-geojson": "URL of a file or name of the page containing GeoJSON data", "maps-fullscreen-button": "Toggle fullscreen", "maps-fullscreen-button-tooltip": "View the map as fullscreen or embedded.", + "maps-mylocation-button-tooltip": "Show my location on the map.", "validation-error-invalid-location": "Parameter \"$1\" must be a valid location.", "validation-error-invalid-locations": "Parameter \"$1\" must be one or more valid locations.", "validation-error-invalid-width": "Parameter \"$1\" must be a valid width.", @@ -125,6 +126,9 @@ "maps-par-height": "Allows setting the height of the map. By default pixels will be assumed as unit, but you can explicitly specify one of these units: px, ex, em.", "maps-par-centre": "The location on which the map should be centered", "maps-par-enable-fullscreen": "Enable fullscreen button", + "maps-par-enable-mylocation": "Enable the geolocation button", + "maps-par-mylocationzoom": "The zoom level to go to when user location is turned on", + "maps-par-enable-mylocationfollow": "Continously center map on user location", "maps-par-kml": "KML files to load onto the map.", "maps-par-markercluster": "Allows merging of multiple nearby markers into one marker", "maps-googlemaps3-incompatbrowser": "Your browser is not compatible with Google Maps v3.", diff --git a/resources/GoogleMaps/googlemaps3ajax.js b/resources/GoogleMaps/googlemaps3ajax.js index aa86319ed..c7b2d7d17 100644 --- a/resources/GoogleMaps/googlemaps3ajax.js +++ b/resources/GoogleMaps/googlemaps3ajax.js @@ -9,7 +9,7 @@ (function( $, sm ) { var ajaxRequest = null; - var mapEvents = ['dragend', 'zoom_changed']; + var mapEvents = ['dragend', 'bounds_changed']; $( document ).ready( function() { // todo: find a way to remove setTimeout. diff --git a/resources/GoogleMaps/img/mylocation-sprite-2x.png b/resources/GoogleMaps/img/mylocation-sprite-2x.png new file mode 100644 index 000000000..546b97a24 Binary files /dev/null and b/resources/GoogleMaps/img/mylocation-sprite-2x.png differ diff --git a/resources/GoogleMaps/jquery.googlemap.js b/resources/GoogleMaps/jquery.googlemap.js index 422c3de79..1c42e5099 100644 --- a/resources/GoogleMaps/jquery.googlemap.js +++ b/resources/GoogleMaps/jquery.googlemap.js @@ -731,6 +731,11 @@ if(options.fullscreen){ this.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(new FullscreenControl(this.map)); } + + // - My Location + if(options.mylocation){ + this.map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(new window.MyLocationControl(this.map, options.mylocationfollow, options.mylocationzoom)); + } }; this.setup = function () { diff --git a/resources/GoogleMaps/mylocation.js b/resources/GoogleMaps/mylocation.js new file mode 100644 index 000000000..3eb91699d --- /dev/null +++ b/resources/GoogleMaps/mylocation.js @@ -0,0 +1,267 @@ +(function( $ ) { + +/* Track user's location with marker on map */ + +function activateTrackMyLocation() { + localStorage.setItem( 'mapsTrackMyLocation', 'yes' ); +} + +function getTrackMyLocation() { + return localStorage.getItem( 'mapsTrackMyLocation' ); +} + +function isTrackMyLocationSet() { + return getTrackMyLocation() && getTrackMyLocation() === 'yes'; +} + +function clearTrackMyLocation() { + localStorage.removeItem( 'mapsTrackMyLocation' ); +} + +/* Follow: Center map on user's location */ + +function activateFollowMyLocation( mapDiv ) { + mapDiv.data( 'followMyLocation', 'locked' ); +} + +function getFollowMyLocation( mapDiv ) { + return mapDiv.data( 'followMyLocation' ); +} + +function isFollowMyLocationSet( mapDiv ) { + return getFollowMyLocation( mapDiv ) && getFollowMyLocation( mapDiv ) === 'locked'; +} + +function clearFollowMyLocation( mapDiv ) { + mapDiv.removeData( 'followMyLocation' ); +} + +function updateMapsTrackMyLocation( centerOnMyLocation = false ) { + $( window.mapsGoogleList ).each( function( index, map ) { + if ( ! map.options.mylocation ) { + return; + } + + let mapDiv = $( map.map.getDiv() ); + + if( isTrackMyLocationSet() ) { + mapDiv.data( 'myLocationIconUI' ).style.backgroundPosition = '-144px 0'; + activateMyLocation( map.map, centerOnMyLocation ); + } else { + mapDiv.data( 'myLocationIconUI' ).style.backgroundPosition = '0 0'; + deactivateMyLocation( map.map ); + } + } ); +} + +$( document ).ready( function() { + // todo: find a way to remove setTimeout. + setTimeout( function() { + if( typeof google === 'undefined' ) { + return; + } + + updateMapsTrackMyLocation( false ); + }, 500 ); +} ); + +// Control for toggling the user location function +function MyLocationControl( map, followMyLocation, zoom ) { + var controlDiv = document.createElement('div'); + controlDiv.style.padding = '10px 10px 0px 10px'; + controlDiv.index = 1; + + var controlUI = document.createElement('div'); + controlUI.style.padding = '6px 6px'; + controlUI.style.backgroundColor = 'white'; + controlUI.style.borderStyle = 'solid'; + controlUI.style.borderColor = 'rgba(0, 0, 0, 0.14902)'; + controlUI.style.borderWidth = '1px'; + controlUI.style.borderRadius = '2px'; + controlUI.style.cursor = 'pointer'; + controlUI.style.textAlign = 'center'; + controlUI.style.boxShadow = 'rgba(0, 0, 0, 0.298039) 0px 1px 4px -1px'; + controlUI.style.backgroundClip = 'padding-box'; + controlUI.title = mw.msg('maps-mylocation-button-tooltip'); + controlDiv.appendChild(controlUI); + + var controlText = document.createElement('div'); + controlText.style.backgroundPosition = '0 0'; + controlText.style.backgroundImage = 'url(' + mw.config.get( 'egMapsScriptPath' ) + '/resources/GoogleMaps/img/mylocation-sprite-2x.png)'; + controlText.style.backgroundSize = '180px 18px'; + controlText.style.display = 'block'; + controlText.style.height = '18px'; + controlText.style.left = '6px'; + controlText.style.margin = '0'; + controlText.style.padding = '0'; + controlText.style.width = '18px'; + controlUI.appendChild(controlText); + + let mapDiv = $( map.getDiv() ); + + // Store for later access + mapDiv.data( 'myLocationIconUI', controlText ); + mapDiv.data( 'myLocationZoom', zoom === -1 ? false : zoom ); + + // Handle toggle button click + google.maps.event.addDomListener( controlUI, 'click', function() { + if ( isTrackMyLocationSet() ) { + clearTrackMyLocation(); + } else { + if ( followMyLocation ) { + activateFollowMyLocation( mapDiv ); + } + + activateTrackMyLocation(); + } + + updateMapsTrackMyLocation( true ); + } ); + + // Handle dragged map + google.maps.event.addDomListener( map, 'dragend', function() { + // Stop centering on user's location + if ( isFollowMyLocationSet( mapDiv ) ) { + clearFollowMyLocation( mapDiv ); + } + } ); + + return controlDiv; +} +window.MyLocationControl = MyLocationControl; + +function handleLocationError( browserHasGeolocation, pos ) { + console.log( browserHasGeolocation ? + 'Error: The Geolocation service failed.' : + 'Error: Your browser doesn\'t support geolocation.' ); +} + +function drawMyLocation( position, map ) { + let pos = { + lat: position.coords.latitude, + lng: position.coords.longitude + }; + let radius = position.coords.accuracy * 0.5; + + let mapDiv = $( map.getDiv() ); + + if ( typeof mapDiv.data( 'myLocationMarker' ) === 'undefined' ) { + // Add a circle to visualize geolocation accuracy + let myLocationCircle = new google.maps.Circle( { + strokeWeight: 0, + fillColor: "#5383EC", + fillOpacity: 0.2, + map, + center: pos, + radius: radius, + } ); + + // Add a marker at the user's location + const svgMarker = { + path: "M 11, 11 m 10, 0 a 10,10 0 1,0 -20,0 a 10,10 0 1,0 20,0", + fillColor: "#5383EC", + fillOpacity: 1, + strokeWeight: 2, + strokeColor: "white", + anchor: new google.maps.Point( 11, 11 ), + scale: 0.75, + }; + + let myLocationMarker = new google.maps.Marker( { + position: pos, + icon: svgMarker, + map: map, + } ); + + // Store for later access + mapDiv.data( 'myLocationMarker', myLocationMarker ); + mapDiv.data( 'myLocationCircle', myLocationCircle ); + } else { + // Update position and radius + mapDiv.data( 'myLocationMarker' ).setPosition( pos ); + mapDiv.data( 'myLocationCircle' ).setCenter( pos ); + mapDiv.data( 'myLocationCircle' ).setRadius( radius ); + } + + if ( isFollowMyLocationSet( mapDiv ) ) { + // Center the map on the user's location + map.setCenter( pos ); + } +} + +function activateMyLocation( map, centerOnMyLocation ) { + let mapDiv = $( map.getDiv() ); + + let geolocationOptions = { + enableHighAccuracy: true, + timeout: 5000, + maximumAge: 0, + }; + + // Check if geolocation is supported + if ( navigator.geolocation ) { + if ( centerOnMyLocation ) { + // Center map only once + navigator.geolocation.getCurrentPosition( + function( position ) { + let pos = { + lat: position.coords.latitude, + lng: position.coords.longitude + }; + map.setCenter( pos ); + + // Zoom into user's location + if ( mapDiv.data( 'myLocationZoom' ) !== false ) { + map.setZoom( mapDiv.data( 'myLocationZoom' ) ); + } + }, + // Error handling + function() { + handleLocationError( true, map.getCenter() ); + }, + geolocationOptions + ); + } + + // Continously track user's location + let myLocationWatchId = navigator.geolocation.watchPosition( + function( position ) { + drawMyLocation( position, map ); + }, + // Error handling + function() { + handleLocationError( true, map.getCenter() ); + }, + geolocationOptions + ); + mapDiv.data( 'myLocationWatchId', myLocationWatchId ); + } else { + // Browser doesn't support geolocation + handleLocationError( false, map.getCenter() ); + } +} + +function deactivateMyLocation( map ) { + let mapDiv = $( map.getDiv() ); + + // Check if geolocation is supported + if ( navigator.geolocation ) { + // Stop tracking location + navigator.geolocation.clearWatch( mapDiv.data( 'myLocationWatchId' ) ); + mapDiv.removeData( 'myLocationWatchId' ); + } + + // Remove marker from the map + if ( typeof mapDiv.data( 'myLocationMarker' ) !== 'undefined' ) { + mapDiv.data( 'myLocationMarker' ).setMap( null ); + mapDiv.removeData( 'myLocationMarker' ); + } + + // Remove circle from the map + if ( typeof mapDiv.data( 'myLocationCircle' ) !== 'undefined' ) { + mapDiv.data( 'myLocationCircle' ).setMap( null ); + mapDiv.removeData( 'myLocationCircle' ); + } +} + +})( window.jQuery ); diff --git a/src/GoogleMapsService.php b/src/GoogleMapsService.php index 8ba39c3fe..22f6ee8b0 100644 --- a/src/GoogleMapsService.php +++ b/src/GoogleMapsService.php @@ -247,6 +247,24 @@ function( string $fileName ) { 'message' => 'maps-par-enable-fullscreen', ]; + $params['mylocation'] = [ + 'type' => 'boolean', + 'default' => false, + 'message' => 'maps-par-enable-mylocation', + ]; + + $params['mylocationfollow'] = [ + 'type' => 'boolean', + 'default' => false, + 'message' => 'maps-par-enable-mylocationfollow', + ]; + + $params['mylocationzoom'] = [ + 'type' => 'integer', + 'default' => -1, + 'message' => 'maps-par-mylocationzoom', + ]; + $params['scrollwheelzoom'] = [ 'aliases' => [ 'scrollzoom' ], 'type' => 'boolean',