diff --git a/docs/references/fit-bounds.md b/docs/references/fit-bounds.md index bdb6b44..1fdeb0f 100644 --- a/docs/references/fit-bounds.md +++ b/docs/references/fit-bounds.md @@ -4,7 +4,7 @@ ## fitBounds -> fitBounds(bounds, zoom = mapOptions.zoom) +> fitBounds(bounds, zoom = mapOptions.zoom, padding = 50) Set zoom and the display area limits of the map. @@ -17,3 +17,15 @@ The display area limits of the map. #### `zoom` - Number The zoom level. + +#### `padding` - Number | Object + +The padding around the display area limits in pixel. +If it's a number, all directions will have the same value. + +Object properties: + +- `top` - Number (optional, default `0`) +- `right` - Number (optional, default `0`) +- `bottom` - Number (optional, default `0`) +- `left` - Number (optional, default `0`) diff --git a/docs/references/index.md b/docs/references/index.md index ff05cf7..74e3690 100644 --- a/docs/references/index.md +++ b/docs/references/index.md @@ -4,36 +4,37 @@ ## Map Provider API -- [Google Map](https://developers.google.com/maps/documentation/javascript/) -- [Mappy (Leaflet)](http://leafletjs.com/reference-1.0.3.html) -- [Leaflet](http://leafletjs.com/reference-1.5.1.html) +- [Google Map](https://developers.google.com/maps/documentation/javascript/) +- [Mappy (Leaflet)](http://leafletjs.com/reference-1.0.3.html) +- [Leaflet](http://leafletjs.com/reference-1.5.1.html) ## Methods -- [addCluster](add-cluster) -- [addMarker](add-marker) -- [addMarkers](add-markers) -- [addUserMarker](add-user-marker) -- [constructor](constructor) -- [extendBounds](extend-bounds) -- [fitBounds](fit-bounds) -- [focusOnMarker](focus-on-marker) -- [getGeolocation](get-geolocation) -- [getMarkerIconByType](get-marker-icon-by-type) -- [getMarkerIcons](get-marker-icons) -- [getMarkerIconType](get-marker-icon-type) -- [getMarker](get-marker) -- [getMarkers](get-markers) -- [getPoints](get-points) -- [getBounds](get-bounds) -- [initMap](init-map) -- [listenZoomChange](listen-zoom-change) -- [minifyMarkerIcons](minify-marker-icons) -- [panTo](pan-to) -- [removeMarker](remove-marker) -- [setCenter](set-center) -- [setIconOnMarker](set-icon-on-marker) -- [setMapOptions](set-map-options) -- [setMarkerIcons](set-marker-icons) -- [setPoint](set-point) -- [setZoom](set-zoom) +- [addCluster](add-cluster) +- [addMarker](add-marker) +- [addMarkers](add-markers) +- [addUserMarker](add-user-marker) +- [constructor](constructor) +- [extendBounds](extend-bounds) +- [fitBounds](fit-bounds) +- [focusOnMarker](focus-on-marker) +- [getGeolocation](get-geolocation) +- [getMarkerIconByType](get-marker-icon-by-type) +- [getMarkerIcons](get-marker-icons) +- [getMarkerIconType](get-marker-icon-type) +- [getMarker](get-marker) +- [getMarkers](get-markers) +- [getPoints](get-points) +- [getBounds](get-bounds) +- [initMap](init-map) +- [listenZoomChange](listen-zoom-change) +- [minifyMarkerIcons](minify-marker-icons) +- [panTo](pan-to) +- [panBy](pan-by) +- [removeMarker](remove-marker) +- [setCenter](set-center) +- [setIconOnMarker](set-icon-on-marker) +- [setMapOptions](set-map-options) +- [setMarkerIcons](set-marker-icons) +- [setPoint](set-point) +- [setZoom](set-zoom) diff --git a/docs/references/pan-by.md b/docs/references/pan-by.md new file mode 100644 index 0000000..6649351 --- /dev/null +++ b/docs/references/pan-by.md @@ -0,0 +1,19 @@ +# References + +[⇠ Go Back](../references/) + +## panBy + +> panBy(x, y) + +Changes the center of the map by the given distance in pixels. + +### Parameters + +#### `x` - Number + +The distance from west to east in pixels. + +#### `y` - Number + +The distance from north to south in pixels. diff --git a/example/components/location.js b/example/components/location.js index b3525ed..d9a5915 100644 --- a/example/components/location.js +++ b/example/components/location.js @@ -48,10 +48,10 @@ class LocationMapModule { }); this.batMap.addMarkers(); - this.batMap.fitBounds( - this.batMap.getBounds(), - this.attr.options.locationZoom, - ); + + const coords = this.attr.locations[0].localisation.coordinates; + this.map.setCenter(this.map.makeLatLng(coords.latitude, coords.longitude)); + this.map.setZoom(this.attr.options.locationZoom); } } diff --git a/example/components/results.js b/example/components/results.js index bc7fb2e..1ad54c9 100644 --- a/example/components/results.js +++ b/example/components/results.js @@ -131,7 +131,14 @@ class ResultsMapModule { this.batMap .getGeolocation() .then(position => { - this.batMap.addUserMarker(position.coords, 'user'); + this.batMap.addUserMarker( + this.map.makeLatLng( + position.coords.latitude, + position.coords.longitude, + ), + 'user', + ); + this.batMap.panToAllMarkers(); }) .catch(error => { console.error('geolocateOnMap(): ' + error.message); @@ -167,6 +174,26 @@ class ResultsMapModule { handleMouseEnterOnMarker(marker) { return () => { + /** + * NOTE: Set all non active markers to default icon for Mappy and Leaflet + * prevent markers to not update correctly when showLabel is set to true + */ + if ( + this.attr.showLabel && + (this.attr.provider === 'mappy' || this.attr.provider === 'leaflet') + ) { + this.getMarkers() + .filter(m => { + const type = this.getMarkerIconType(m); + return ( + type !== 'active' && type !== 'default' && marker.id !== m.id + ); + }) + .forEach(m => { + this.setIconOnMarker(m, 'default'); + }); + } + if ( this.getMarkerIconType(marker) !== 'active' && this.getMarkerIconType(marker) !== 'hover' diff --git a/src/AbstractMap.js b/src/AbstractMap.js index a2df894..29c636d 100644 --- a/src/AbstractMap.js +++ b/src/AbstractMap.js @@ -33,6 +33,7 @@ export class AbstractMap { this.icons = []; this.bounds = null; this.cluster = null; + this.focusInProgress = false; this.defaultOptions = { zoom: 12, @@ -110,6 +111,10 @@ export class AbstractMap { console.error(`${this.provider} has no 'setPoint' method implemented.`); } + clearPoints() { + this.points.length = 0; + } + addMarkers() { console.error(`${this.provider} has no 'addMarkers' method implemented.`); } @@ -128,6 +133,21 @@ export class AbstractMap { console.error(`${this.provider} has no 'removeMarker' method implemented.`); } + removeCluster() { + console.error( + `${this.provider} has no 'removeCluster' method implemented.`, + ); + } + + removeAllMarkers() { + if (this.cluster) { + this.removeCluster(); + } + this.markers.forEach(marker => { + this.removeMarker(marker); + }); + } + setMarkerIcons() { console.error( `${this.provider} has no 'setMarkerIcons' method implemented.`, @@ -150,10 +170,20 @@ export class AbstractMap { console.error(`${this.provider} has no 'addCluster' method implemented.`); } + makeLatLng() { + console.error(`${this.provider} has no 'makeLatLng' method implemented.`); + } + setCenter() { console.error(`${this.provider} has no 'setCenter' method implemented.`); } + getCenterLatLng() { + console.error( + `${this.provider} has no 'getCenterLatLng' method implemented.`, + ); + } + fitBounds() { console.error(`${this.provider} has no 'fitBounds' method implemented.`); } @@ -166,10 +196,24 @@ export class AbstractMap { console.error(`${this.provider} has no 'getBounds' method implemented.`); } + getBoundsLatLng() { + console.error( + `${this.provider} has no 'getBoundsLatLng' method implemented.`, + ); + } + panTo() { console.error(`${this.provider} has no 'panTo' method implemented.`); } + panBy() { + console.error(`${this.provider} has no 'panBy' method implemented.`); + } + + getZoom() { + console.error(`${this.provider} has no 'getZoom' method implemented.`); + } + setZoom() { console.error(`${this.provider} has no 'setZoom' method implemented.`); } @@ -180,6 +224,12 @@ export class AbstractMap { ); } + listenBoundsChange() { + console.error( + `${this.provider} has no 'listenBoundsChange' method implemented.`, + ); + } + minifyMarkerIcons() { console.error( `${this.provider} has no 'minifyMarkerIcons' method implemented.`, diff --git a/src/providers/gmaps/index.js b/src/providers/gmaps/index.js index 102abb3..ab1b401 100644 --- a/src/providers/gmaps/index.js +++ b/src/providers/gmaps/index.js @@ -42,6 +42,10 @@ export default class GoogleMaps extends AbstractMap { 'https://maps.googleapis.com/maps/api/js', this.apiKey[1], ); + + if (this.apiKey.length > 2) { + urlParams = urlParams + '&channel=' + this.apiKey[2]; + } } else { urlParams = urlParams + '&key=' + this.apiKey; } @@ -79,6 +83,7 @@ export default class GoogleMaps extends AbstractMap { initMap() { this.bounds = new google.maps.LatLngBounds(); this.map = new google.maps.Map(this.domElement, this.mapOptions); + this.initialBoundsEvent = true; } setPoint(location, iconType, label = false) { @@ -112,9 +117,11 @@ export default class GoogleMaps extends AbstractMap { } addMarker(point, eventCallback = {}) { - const marker = new google.maps.Marker(point); + const marker = new google.maps.Marker({ + map: this.map, + ...point, + }); this.markers.push(marker); - marker.setMap(this.map); this.setIconOnMarker(marker, point.iconType); @@ -127,12 +134,16 @@ export default class GoogleMaps extends AbstractMap { } removeMarker(marker) { - marker = this.getMarker(marker); + marker = marker ? marker : this.getMarker(marker); marker.setMap(null); this.markers = this.markers.filter(m => m.id !== marker.id); } + removeCluster() { + this.cluster.clearMarkers(); + } + setMarkerIcons() { Object.keys(this.markersOptions).forEach(type => { const options = this.markersOptions[type]; @@ -187,19 +198,37 @@ export default class GoogleMaps extends AbstractMap { } } - focusOnMarker(marker) { + focusOnMarker(marker, offset = { x: 0, y: 0 }) { + this.focusInProgress = true; + let hasOffset = offset.x || offset.y; marker = this.getMarker(marker); + const listener = this.map.addListener('idle', () => { + if (hasOffset) { + hasOffset = false; + this.map.panBy(offset.x, offset.y); + } else { + this.focusInProgress = false; + google.maps.event.removeListener(listener); + } + }); + this.map.setZoom(this.mapOptions.locationZoom); this.panTo(marker.position); } addUserMarker(position, iconType, id = 0) { if (position) { + // Backward compatibility + const latLng = + position.latitude && position.longitude + ? this.makeLatLng(position.latitude, position.longitude) + : position; + const point = { id: `${id}`, map: this.map, - position: new google.maps.LatLng(position.latitude, position.longitude), + position: latLng, iconType, }; @@ -226,10 +255,12 @@ export default class GoogleMaps extends AbstractMap { width: icon.scaledSize.width, height: icon.scaledSize.height, anchorText: [ - icon.labelOrigin.y - - icon.scaledSize.height / 2 + - icon.labelOptions.size * 1.2, - icon.labelOrigin.x - icon.scaledSize.width / 2, + Math.ceil( + icon.labelOrigin.y - + icon.scaledSize.height / 2 + + icon.labelOptions.size * 1.2, + ), + Math.ceil(icon.labelOrigin.x - icon.scaledSize.width / 2), ], // [yoffset, xoffset] anchorIcon: [icon.anchor.y, icon.anchor.x], // [yoffset, xoffset] textSize: icon.labelOptions.size, @@ -245,56 +276,112 @@ export default class GoogleMaps extends AbstractMap { ); } + getZoom() { + return this.map.getZoom(); + } + setZoom(zoom) { this.map.setZoom(zoom); } + makeLatLng(latitude, longitude) { + return new google.maps.LatLng(latitude, longitude); + } + setCenter(position) { this.map.setCenter(position); } + getCenterLatLng() { + const center = this.map.getCenter(); + return { + lat: center.lat(), + lng: center.lng(), + }; + } + getBounds() { return this.bounds; } + getBoundsLatLng() { + const bounds = this.map.getBounds(); + const northEast = bounds.getNorthEast(); + const southWest = bounds.getSouthWest(); + return [southWest.lat(), southWest.lng(), northEast.lat(), northEast.lng()]; + } + extendBounds(position) { return this.bounds.extend(position); } - fitBounds(bounds, zoom = this.mapOptions.zoom) { - if (this.markers.length > 1) { - this.map.fitBounds(bounds); - } else { - this.setCenter(this.markers[0].position); - this.setZoom(zoom); - } + fitBounds(bounds, _zoom, padding = 50) { + this.map.fitBounds(bounds, padding); } panTo(position) { this.map.panTo(position); } + panBy(x, y) { + this.map.panBy(x, y); + } + listenZoomChange(callback) { this.map.addListener('zoom_changed', () => { return callback(this.map.getZoom()); }); } + listenBoundsChange(callback, ignoreFocusOnMarker = true) { + this.map.addListener('bounds_changed', () => { + if (ignoreFocusOnMarker && this.focusInProgress) { + return; + } + if (this.initialBoundsEvent) { + this.initialBoundsEvent = false; + return; + } + return callback(this.getCenterLatLng()); + }); + } + minifyMarkerIcons(zoom, breakZoom = 8, minifier = 0.8) { if (zoom < breakZoom + 1 && !this.isMinifiedMarkerIcons) { [].forEach.call(Object.keys(this.icons), key => { const size = this.icons[key].scaledSize; - this.icons[key].scaledSize.width = size.width * minifier; - this.icons[key].scaledSize.height = size.height * minifier; + const width = size.width * minifier; + const height = size.height * minifier; + + this.icons[key].scaledSize = new google.maps.Size(width, height); + this.icons[key].anchor = new google.maps.Point(width / 2, height); }); + this.refreshAllMarkers(); + this.isMinifiedMarkerIcons = true; } else if (zoom > breakZoom && this.isMinifiedMarkerIcons) { [].forEach.call(Object.keys(this.icons), key => { const size = this.icons[key].scaledSize; - this.icons[key].scaledSize.width = size.width / minifier; - this.icons[key].scaledSize.height = size.height / minifier; + const width = size.width / minifier; + const height = size.height / minifier; + + this.icons[key].scaledSize = new google.maps.Size(width, height); + this.icons[key].anchor = new google.maps.Point(width / 2, height); }); + this.refreshAllMarkers(); + this.isMinifiedMarkerIcons = false; } } + + refreshAllMarkers() { + this.getMarkers().forEach(marker => { + const iconName = this.getMarkerIconType(marker); + marker.setIcon(this.icons[iconName]); + }); + + if (this.userMarker) { + this.userMarker.setIcon(this.icons.user); + } + } } diff --git a/src/providers/leaflet/index.js b/src/providers/leaflet/index.js index a155a82..1cfe58b 100644 --- a/src/providers/leaflet/index.js +++ b/src/providers/leaflet/index.js @@ -128,8 +128,7 @@ export default class Leaflet extends AbstractMap { const marker = L.marker(point.position, point); marker.id = point.id; marker.location = point.location; - - this.setIconOnMarker(marker, point.iconType); + marker.options.alt = 'marker ' + point.location.name; if (this.showCluster && this.icons.cluster) { this.cluster.addLayer(marker); @@ -145,6 +144,8 @@ export default class Leaflet extends AbstractMap { this.extendBounds(marker.getLatLng()); this.markers.push(marker); + + this.setIconOnMarker(marker, point.iconType); } removeMarker(marker) { @@ -154,6 +155,10 @@ export default class Leaflet extends AbstractMap { this.markers = this.markers.filter(m => m.id !== marker.id); } + removeCluster() { + this.cluster.remove(); + } + setMarkerIcons() { Object.keys(this.markersOptions).forEach(type => { const options = this.markersOptions[type]; @@ -207,7 +212,7 @@ export default class Leaflet extends AbstractMap { className: icon.options.className, iconSize: icon.options.iconSize, iconAnchor: icon.options.iconAnchor, - html: `${span.outerHTML}`, + html: `${marker.options.alt}${span.outerHTML}`, }), ); } else { @@ -216,17 +221,34 @@ export default class Leaflet extends AbstractMap { } } - focusOnMarker(marker) { + focusOnMarker(marker, offset = { x: 0, y: 0 }) { + this.focusInProgress = true; + let hasOffset = offset.x || offset.y; marker = this.getMarker(marker); + const onMoveEnd = () => { + if (hasOffset) { + hasOffset = false; + this.map.panBy(new L.Point(offset.x, offset.y)); + } else { + this.focusInProgress = false; + this.map.off('moveend', onMoveEnd, this); + } + }; + this.map.on('moveend', onMoveEnd, this); + this.panTo(marker.getLatLng()); } addUserMarker(position, iconType, id = 0) { if (position) { - this.userMarker = new L.marker( - L.latLng(position.latitude, position.longitude), - ); + // Backward compatibility + const latLng = + position.latitude && position.longitude + ? this.makeLatLng(position.latitude, position.longitude) + : position; + + this.userMarker = new L.marker(latLng); this.userMarker.id = id; this.userMarker.addTo(this.map); @@ -274,74 +296,120 @@ export default class Leaflet extends AbstractMap { this.map.addLayer(this.cluster); } + getZoom() { + return this.map.getZoom(); + } + setZoom(zoom) { this.map.setZoom(zoom); } + makeLatLng(latitude, longitude) { + return L.latLng(latitude, longitude); + } + setCenter(position, zoom = this.mapOptions.zoom) { this.map.setView(position, zoom); } + getCenterLatLng() { + return this.map.getCenter(); + } + getBounds() { return this.bounds; } + getBoundsLatLng() { + const bounds = this.map.getBounds(); + const northEast = bounds.getNorthEast(); + const southWest = bounds.getSouthWest(); + return [southWest.lat, southWest.lng, northEast.lat, northEast.lng]; + } + extendBounds(position) { return this.bounds.extend(position); } - fitBounds(bounds, zoom = this.mapOptions.zoom) { - if (this.markers.length > 1) { - this.map.fitBounds(bounds, { - padding: L.point(50, 50), - maxZoom: zoom, - }); + fitBounds(bounds, zoom = this.mapOptions.zoom, padding = 50) { + const options = { + maxZoom: zoom, + }; + + if (!isNaN(padding)) { + options['padding'] = L.point(padding, padding); } else { - this.setCenter(this.markers[0].getLatLng(), zoom); + options['paddingTopLeft'] = L.point(padding.left || 0, padding.top || 0); + options['paddingBottomRight'] = L.point( + padding.right || 0, + padding.bottom || 0, + ); } + + this.map.fitBounds(bounds, options); } panTo(position, zoom = this.mapOptions.locationZoom) { this.map.flyTo(position, zoom); } + panBy(x, y) { + this.map.panBy(L.point(x, y)); + } + listenZoomChange(callback) { this.map.on('zoomend', () => { return callback(this.map.getZoom()); }); } + listenBoundsChange(callback, ignoreFocusOnMarker = true) { + this.map.on('move', () => { + if (ignoreFocusOnMarker && this.focusInProgress) { + return; + } + return callback(this.getCenterLatLng()); + }); + } + minifyMarkerIcons(zoom, breakZoom = 8, minifier = 0.8) { if (zoom < breakZoom + 1 && !this.isMinifiedMarkerIcons) { [].forEach.call(Object.keys(this.icons), key => { const size = this.icons[key].options.iconSize; - this.icons[key].options.iconSize = [ - size[0] * minifier, - size[1] * minifier, - ]; + const width = size[0] * minifier; + const height = size[1] * minifier; + + this.icons[key].options.iconSize = [width, height]; + this.icons[key].options.iconAnchor = [width / 2, height]; }); + + this.refreshAllMarkers(); + this.isMinifiedMarkerIcons = true; - this.updateAllMarkerIconsOnMap(); } else if (zoom > breakZoom && this.isMinifiedMarkerIcons) { [].forEach.call(Object.keys(this.icons), key => { const size = this.icons[key].options.iconSize; - this.icons[key].options.iconSize = [ - size[0] / minifier, - size[1] / minifier, - ]; + const width = size[0] / minifier; + const height = size[1] / minifier; + + this.icons[key].options.iconSize = [width, height]; + this.icons[key].options.iconAnchor = [width / 2, height]; }); + + this.refreshAllMarkers(); + this.isMinifiedMarkerIcons = false; - this.updateAllMarkerIconsOnMap(); } } - updateAllMarkerIconsOnMap() { - [].forEach.call(this.markers, marker => { - this.setIconOnMarker(marker, marker.iconType, false); + refreshAllMarkers() { + this.getMarkers().forEach(marker => { + const iconName = this.getMarkerIconType(marker); + marker.setIcon(this.icons[iconName]); }); if (this.userMarker) { - this.setIconOnMarker(this.userMarker, this.userMarker.iconType, false); + this.userMarker.setIcon(this.icons.user); } } } diff --git a/src/providers/mappy-widget/index.js b/src/providers/mappy-widget/index.js index 5f35589..7c1ff49 100644 --- a/src/providers/mappy-widget/index.js +++ b/src/providers/mappy-widget/index.js @@ -143,6 +143,7 @@ export default class MappyWidget extends AbstractMap { const marker = L.marker(point.position, point); marker.id = point.id; marker.location = point.location; + marker.options.alt = 'marker ' + point.location.name; this.setIconOnMarker(marker, point.iconType); @@ -225,7 +226,7 @@ export default class MappyWidget extends AbstractMap { className: icon.options.className, iconSize: icon.options.iconSize, iconAnchor: icon.options.iconAnchor, - html: `${span.outerHTML}`, + html: `${marker.options.alt}${span.outerHTML}`, }), ); } else { diff --git a/src/providers/mappy/index.js b/src/providers/mappy/index.js index 7bbb7e9..5a9235f 100644 --- a/src/providers/mappy/index.js +++ b/src/providers/mappy/index.js @@ -9,6 +9,8 @@ import objectAssign from 'object-assign'; import { AbstractMap } from '../../AbstractMap'; import { domUtils, loaderUtils } from '../../utils'; +let L; + export default class Mappy extends AbstractMap { constructor(...args) { super(...args); @@ -65,8 +67,8 @@ export default class Mappy extends AbstractMap { } domUtils.addResources(document.head, resources, () => { - this.L = window.L; - this.L.Mappy.setImgPath( + L = window.L; + L.Mappy.setImgPath( '//d11lbkprc85eyb.cloudfront.net/Mappy/7.5.0/images/', ); callback(); @@ -107,13 +109,13 @@ export default class Mappy extends AbstractMap { } initMap() { - this.bounds = new this.L.latLngBounds([]); - this.map = new this.L.Mappy.Map(this.domElement, this.mapOptions); + this.bounds = new L.latLngBounds([]); + this.map = new L.Mappy.Map(this.domElement, this.mapOptions); } setPoint(location, iconType, label = false) { const point = { - position: this.L.latLng( + position: L.latLng( location.localisation.coordinates.latitude, location.localisation.coordinates.longitude, ), @@ -140,11 +142,10 @@ export default class Mappy extends AbstractMap { } addMarker(point, eventCallback = {}) { - const marker = this.L.marker(point.position, point); + const marker = L.marker(point.position, point); marker.id = point.id; marker.location = point.location; - - this.setIconOnMarker(marker, point.iconType); + marker.options.alt = 'marker ' + point.location.name; if (this.showCluster && this.icons.cluster) { this.cluster.addLayer(marker); @@ -160,6 +161,8 @@ export default class Mappy extends AbstractMap { this.extendBounds(marker.getLatLng()); this.markers.push(marker); + + this.setIconOnMarker(marker, point.iconType); } removeMarker(marker) { @@ -169,13 +172,17 @@ export default class Mappy extends AbstractMap { this.markers = this.markers.filter(m => m.id !== marker.id); } + removeCluster() { + this.cluster.remove(); + } + setMarkerIcons() { Object.keys(this.markersOptions).forEach(type => { const options = this.markersOptions[type]; const iconAnchor = options.anchor || [options.width / 2, options.height]; const iconLabelOptions = options.label || {}; - this.icons[type] = new this.L.Icon({ + this.icons[type] = new L.Icon({ className: `batmap-marker-${type}`, iconUrl: options.url, iconSize: [options.width, options.height], @@ -218,11 +225,11 @@ export default class Mappy extends AbstractMap { span.style.fontSize = `${labelOptions.size}px`; marker.setIcon( - new this.L.DivIcon({ + new L.DivIcon({ className: icon.options.className, iconSize: icon.options.iconSize, iconAnchor: icon.options.iconAnchor, - html: `${span.outerHTML}`, + html: `${marker.options.alt}${span.outerHTML}`, }), ); } else { @@ -231,17 +238,34 @@ export default class Mappy extends AbstractMap { } } - focusOnMarker(marker) { + focusOnMarker(marker, offset = { x: 0, y: 0 }) { + this.focusInProgress = true; + let hasOffset = offset.x || offset.y; marker = this.getMarker(marker); + const onMoveEnd = () => { + if (hasOffset) { + hasOffset = false; + this.map.panBy(offset); + } else { + this.focusInProgress = false; + this.map.off('moveend', onMoveEnd, this); + } + }; + this.map.on('moveend', onMoveEnd, this); + this.panTo(marker.getLatLng()); } addUserMarker(position, iconType, id = 0) { if (position) { - this.userMarker = new this.L.marker( - this.L.latLng(position.latitude, position.longitude), - ); + // Backward compatibility + const latLng = + position.latitude && position.longitude + ? this.makeLatLng(position.latitude, position.longitude) + : position; + + this.userMarker = new L.marker(latLng); this.userMarker.id = id; this.userMarker.addTo(this.map); @@ -253,7 +277,7 @@ export default class Mappy extends AbstractMap { addCluster() { const icon = this.icons.cluster; - this.cluster = this.L.markerClusterGroup( + this.cluster = L.markerClusterGroup( objectAssign( { showCoverageOnHover: false, @@ -273,7 +297,7 @@ export default class Mappy extends AbstractMap { span.style.fontWeight = `${labelOptions.weight}`; span.style.fontSize = `${labelOptions.size}px`; - return this.L.divIcon({ + return L.divIcon({ className: icon.options.className, html: `` + @@ -289,74 +313,120 @@ export default class Mappy extends AbstractMap { this.map.addLayer(this.cluster); } + getZoom() { + return this.map.getZoom(); + } + setZoom(zoom) { this.map.setZoom(zoom); } + makeLatLng(latitude, longitude) { + return L.latLng(latitude, longitude); + } + setCenter(position, zoom = this.mapOptions.zoom) { this.map.setView(position, zoom); } + getCenterLatLng() { + return this.map.getCenter(); + } + getBounds() { return this.bounds; } + getBoundsLatLng() { + const bounds = this.map.getBounds(); + const northEast = bounds.getNorthEast(); + const southWest = bounds.getSouthWest(); + return [southWest.lat, southWest.lng, northEast.lat, northEast.lng]; + } + extendBounds(position) { return this.bounds.extend(position); } - fitBounds(bounds, zoom = this.mapOptions.zoom) { - if (this.markers.length > 1) { - this.map.fitBounds(bounds, { - padding: this.L.point(50, 50), - maxZoom: zoom, - }); + fitBounds(bounds, zoom = this.mapOptions.zoom, padding = 50) { + const options = { + maxZoom: zoom, + }; + + if (!isNaN(padding)) { + options['padding'] = L.point(padding, padding); } else { - this.setCenter(this.markers[0].getLatLng(), zoom); + options['paddingTopLeft'] = L.point(padding.left || 0, padding.top || 0); + options['paddingBottomRight'] = L.point( + padding.right || 0, + padding.bottom || 0, + ); } + + this.map.fitBounds(bounds, options); } panTo(position, zoom = this.mapOptions.locationZoom) { this.map.setView(position, zoom); } + panBy(x, y) { + this.map.panBy(L.point(x, y)); + } + listenZoomChange(callback) { this.map.on('zoomend', () => { return callback(this.map.getZoom()); }); } + listenBoundsChange(callback, ignoreFocusOnMarker = true) { + this.map.on('move', () => { + if (ignoreFocusOnMarker && this.focusInProgress) { + return; + } + return callback(this.getCenterLatLng()); + }); + } + minifyMarkerIcons(zoom, breakZoom = 8, minifier = 0.8) { if (zoom < breakZoom + 1 && !this.isMinifiedMarkerIcons) { [].forEach.call(Object.keys(this.icons), key => { const size = this.icons[key].options.iconSize; - this.icons[key].options.iconSize = [ - size[0] * minifier, - size[1] * minifier, - ]; + const width = size[0] * minifier; + const height = size[1] * minifier; + + this.icons[key].options.iconSize = [width, height]; + this.icons[key].options.iconAnchor = [width / 2, height]; }); + + this.refreshAllMarkers(); + this.isMinifiedMarkerIcons = true; - this.updateAllMarkerIconsOnMap(); } else if (zoom > breakZoom && this.isMinifiedMarkerIcons) { [].forEach.call(Object.keys(this.icons), key => { const size = this.icons[key].options.iconSize; - this.icons[key].options.iconSize = [ - size[0] / minifier, - size[1] / minifier, - ]; + const width = size[0] / minifier; + const height = size[1] / minifier; + + this.icons[key].options.iconSize = [width, height]; + this.icons[key].options.iconAnchor = [width / 2, height]; }); + + this.refreshAllMarkers(); + this.isMinifiedMarkerIcons = false; - this.updateAllMarkerIconsOnMap(); } } - updateAllMarkerIconsOnMap() { - [].forEach.call(this.markers, marker => { - this.setIconOnMarker(marker, marker.iconType, false); + refreshAllMarkers() { + this.getMarkers().forEach(marker => { + const iconName = this.getMarkerIconType(marker); + marker.setIcon(this.icons[iconName]); }); if (this.userMarker) { - this.setIconOnMarker(this.userMarker, this.userMarker.iconType, false); + this.userMarker.setIcon(this.icons.user); } } }