Skip to content

Instantly share code, notes, and snippets.

@joegaudet
Created September 5, 2018 01:14
Show Gist options
  • Save joegaudet/ea24a28c34fae003d4238036ed4efa7a to your computer and use it in GitHub Desktop.
Save joegaudet/ea24a28c34fae003d4238036ed4efa7a to your computer and use it in GitHub Desktop.
import Ember from 'ember';
import ENV from 'star-fox/config/environment';
const {
Object,
Component,
run,
observer
} = Ember;
const {
Map,
Polygon
} = google.maps;
/**
* This component wraps the google map view and allows for simple editing of a polygon
*/
export default Component.extend({
classNames: 'fde-map-control',
/** @type {LatLng[]} */
value: [],
/**
* @callback changeHandler
* @property {LatLong[]} startDate as a string
*
* @type {changeHandler}
*/
onChange(){},
/** @type {string} */
citySearchText: '',
/** @type {boolean} */
allowsPolygonEditing: true,
didInsertElement(){
const canvas = this.$('.g-map-canvas').get(0);
const latLngs = this.get('value').map(({lat, long}) => ({lat, lng: long}));
// center of a polygon... roughly is the average of the lats and longs
// now that I think about it, it's probably better to find the min and
// maxes and divide by / 2 but this actually work most of the time :P
const coords = latLngs.reduce((acc, latLng) => {
acc.centerLat += latLng.lat;
acc.centerLng += latLng.lng;
return acc;
}, {
centerLat: 0,
centerLng: 0
});
coords.centerLat /= latLngs.length;
coords.centerLng /= latLngs.length;
// construct the map
const map = new Map(canvas, {
// protocol: 'https',
key: ENV['g-map'].key,
libraries: ['places', 'geometry'],
zoom: 12,
center: {lat: coords.centerLat || 0, lng: coords.centerLng || 0},
mapTypeId: 'terrain'
});
this.set('map', map);
this._fitToBounds(latLngs);
// init places service for handling the map lookups
const placesService = new google.maps.places.PlacesService(map);
this.set('placesService', placesService);
if(this.get('allowsPolygonEditing')){
// Construct the polygon.
const polygon = new Polygon({
paths: latLngs,
editable: !this.get('isReadonly'),
strokeColor: '#FF0000',
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: '#FF0000',
fillOpacity: 0.35
});
this.set('polygon', polygon);
polygon.setMap(map);
// setup events
this.setupPolygonEvents();
}
},
willDestroyElement(){
const polygon = this.get('polygon');
const map = this.get('polygon');
// cleanup
google.maps.event.clearListeners(polygon);
google.maps.event.clearListeners(map);
delete this.map;
delete this.polygon;
},
/**
*
*/
setupPolygonEvents: function () {
const polygon = this.get('polygon');
/**
* Register right click handler for deleting verticies
*/
google.maps.event.addListener(polygon, 'rightclick', this._handleRightClick.bind(this));
/**
* Register mouse up to fire change events
*/
google.maps.event.addListener(polygon, 'mouseup', this._handleMouseUp.bind(this));
},
/**
* @param {event} e
* @private
*/
_handleRightClick(e){
const map = this.get('map');
const polygon = this.get('polygon');
const deleteMenu = new DeleteMenu(this);
// Check if click was on a vertex control point
if (e.vertex == undefined) {
return;
}
deleteMenu.open(map, polygon.getPath(), e.vertex);
},
/**
* @private
*/
_handleMouseUp(){
run.next(_ => this.fireOnChange());
},
/**
* @param {LatLng[]} latLngs
* @private
*/
_fitToBounds(latLngs) {
if (latLngs.length > 0) {
const bounds = new google.maps.LatLngBounds();
latLngs.forEach((point) => bounds.extend(point));
this.get('map').fitBounds(bounds);
}
},
/**
* Observer for watching incoming changes and translating them to lat lng and updating the map
*/
valueDidChange: observer('value', function () {
const latLngs = this.get('value').map(({lat, long}) => ({lat, lng: long}));
this.get('polygon')
.setPath(latLngs);
}),
/**
* Push onChange events to lat / long which is being used throughout starfox
*/
fireOnChange() {
const newValue = this
.get('polygon')
// get the polygon path in an es6 array
.getPath()
.getArray()
// We use Lat and Long pretty much everywhere an google likes the symetry of lat/lng
.map(_ => Object.create({lat: _.lat(), long: _.lng()}));
this.onChange(newValue);
},
actions: {
/**
* Searches for a city and centers the map on that place (the city could be any google place)
*/
centerOnCity(){
const citySearchText = this.get('citySearchText');
if (citySearchText) {
const request = {
query: citySearchText
};
this.get('placesService')
.textSearch(request, (results, status) => {
if (status == google.maps.places.PlacesServiceStatus.OK) {
const {geometry: {location: {lat, lng}}} = results[0];
const map = this.get('map');
map.setCenter({lat: lat(), lng: lng()});
map.setZoom(12);
}
});
}
},
/**
* Set the polygon to a box around the center of the map.
*/
setBoxAroundCenter() {
const map = this.get('map');
const center = map.getCenter();
const x = center.lng();
const y = center.lat();
const box = [
{lat: y - 0.05, lng: x - 0.05},
{lat: y - 0.05, lng: x + 0.05},
{lat: y + 0.05, lng: x + 0.05},
{lat: y + 0.05, lng: x - 0.05},
{lat: y - 0.05, lng: x - 0.05}
];
this
.get('polygon')
.setPath(box);
this.fireOnChange();
}
}
});
/**
* A menu that lets a user delete a selected vertex of a path. I stole this from the google examples
* It's not ember, but will only be used inside the context of this app.
*
* https://developers.google.com/maps/documentation/javascript/examples/delete-vertex-menuA
*
* @constructor
*/
function DeleteMenu(component) {
this._component = component;
this.div_ = document.createElement('div');
this.div_.className = 'fde-gmap-delete-menu';
this.div_.innerHTML = 'Delete';
var menu = this;
google.maps.event.addDomListener(this.div_, 'click', function () {
menu.removeVertex();
});
}
DeleteMenu.prototype = new google.maps.OverlayView();
DeleteMenu.prototype.onAdd = function () {
var deleteMenu = this;
var map = this.getMap();
this.getPanes().floatPane.appendChild(this.div_);
// mousedown anywhere on the map except on the menu div will close the
// menu.
this.divListener_ = google.maps.event.addDomListener(map.getDiv(), 'mousedown', function (e) {
if (e.target != deleteMenu.div_) {
deleteMenu.close();
}
}, true);
};
DeleteMenu.prototype.onRemove = function () {
google.maps.event.removeListener(this.divListener_);
this.div_.parentNode.removeChild(this.div_);
this._component.fireOnChange();
// clean up
this.set('position');
this.set('path');
this.set('vertex');
};
DeleteMenu.prototype.close = function () {
this.setMap(null);
};
DeleteMenu.prototype.draw = function () {
var position = this.get('position');
var projection = this.getProjection();
if (!position || !projection) {
return;
}
var point = projection.fromLatLngToDivPixel(position);
this.div_.style.top = point.y + 'px';
this.div_.style.left = point.x + 'px';
};
/**
* Opens the menu at a vertex of a given path.
*/
DeleteMenu.prototype.open = function (map, path, vertex) {
this.set('position', path.getAt(vertex));
this.set('path', path);
this.set('vertex', vertex);
this.setMap(map);
this.draw();
};
/**
* Deletes the vertex from the path.
*/
DeleteMenu.prototype.removeVertex = function () {
var path = this.get('path');
var vertex = this.get('vertex');
if (!path || vertex == undefined) {
this.close();
return;
}
path.removeAt(vertex);
this.close();
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment