mirror of
https://github.com/librenms/librenms.git
synced 2024-09-21 18:38:25 +00:00
957ecc2441
* Better handling of errors Mapquest seems to return the center of the US on error....... * Editable locations WIP * Change to bootgrid ajax table WIP * Graphs working, using handlebars update js libs add current location button * remove sql query, change icon * Add the map to the device view, only when gps is expanded. Allow edit on device page, share js code * fix chevron rotation, improve click area * extra warning * fix overview layout (remove containers) * fix style * fix html divs, change collapse ui a bit move css, update css/js versions * start zoomed out on new locations * don't double load scripts, zoom to 17 * fix php-md errors, remove unused use statement * improve non-admin experience * Move locations page to Laravel More functions in Url and Html util classes reduce code duplication * translation buttons too * fix whitespace * move formatters to the frontend * small changes * disable traffic for locations with no devices * change down 0 to green from gray * missing " * Fix paginate all * working fix for paginate all * allow sort by counts * fix down sort * a little safety * Don't call the function twice * btn-xs
458 lines
14 KiB
JavaScript
458 lines
14 KiB
JavaScript
// Based on https://github.com/shramov/leaflet-plugins
|
|
// GridLayer like https://avinmathew.com/leaflet-and-google-maps/ , but using MutationObserver instead of jQuery
|
|
|
|
|
|
// 🍂class GridLayer.GoogleMutant
|
|
// 🍂extends GridLayer
|
|
L.GridLayer.GoogleMutant = L.GridLayer.extend({
|
|
options: {
|
|
minZoom: 0,
|
|
maxZoom: 23,
|
|
tileSize: 256,
|
|
subdomains: 'abc',
|
|
errorTileUrl: '',
|
|
attribution: '', // The mutant container will add its own attribution anyways.
|
|
opacity: 1,
|
|
continuousWorld: false,
|
|
noWrap: false,
|
|
// 🍂option type: String = 'roadmap'
|
|
// Google's map type. Valid values are 'roadmap', 'satellite' or 'terrain'. 'hybrid' is not really supported.
|
|
type: 'roadmap',
|
|
maxNativeZoom: 21
|
|
},
|
|
|
|
initialize: function (options) {
|
|
L.GridLayer.prototype.initialize.call(this, options);
|
|
|
|
this._ready = !!window.google && !!window.google.maps && !!window.google.maps.Map;
|
|
|
|
this._GAPIPromise = this._ready ? Promise.resolve(window.google) : new Promise(function (resolve, reject) {
|
|
var checkCounter = 0;
|
|
var intervalId = null;
|
|
intervalId = setInterval(function () {
|
|
if (checkCounter >= 10) {
|
|
clearInterval(intervalId);
|
|
return reject(new Error('window.google not found after 10 attempts'));
|
|
}
|
|
if (!!window.google && !!window.google.maps && !!window.google.maps.Map) {
|
|
clearInterval(intervalId);
|
|
return resolve(window.google);
|
|
}
|
|
checkCounter++;
|
|
}, 500);
|
|
});
|
|
|
|
// Couple data structures indexed by tile key
|
|
this._tileCallbacks = {}; // Callbacks for promises for tiles that are expected
|
|
this._freshTiles = {}; // Tiles from the mutant which haven't been requested yet
|
|
|
|
this._imagesPerTile = (this.options.type === 'hybrid') ? 2 : 1;
|
|
|
|
this._boundOnMutatedImage = this._onMutatedImage.bind(this);
|
|
},
|
|
|
|
onAdd: function (map) {
|
|
L.GridLayer.prototype.onAdd.call(this, map);
|
|
this._initMutantContainer();
|
|
|
|
this._GAPIPromise.then(function () {
|
|
this._ready = true;
|
|
this._map = map;
|
|
|
|
this._initMutant();
|
|
|
|
map.on('viewreset', this._reset, this);
|
|
map.on('move', this._update, this);
|
|
map.on('zoomend', this._handleZoomAnim, this);
|
|
map.on('resize', this._resize, this);
|
|
|
|
//handle layer being added to a map for which there are no Google tiles at the given zoom
|
|
google.maps.event.addListenerOnce(this._mutant, 'idle', function () {
|
|
this._checkZoomLevels();
|
|
this._mutantIsReady = true;
|
|
}.bind(this));
|
|
|
|
//20px instead of 1em to avoid a slight overlap with google's attribution
|
|
map._controlCorners.bottomright.style.marginBottom = '20px';
|
|
map._controlCorners.bottomleft.style.marginBottom = '20px';
|
|
|
|
this._reset();
|
|
this._update();
|
|
|
|
if (this._subLayers) {
|
|
//restore previously added google layers
|
|
for (var layerName in this._subLayers) {
|
|
this._subLayers[layerName].setMap(this._mutant);
|
|
}
|
|
}
|
|
}.bind(this));
|
|
},
|
|
|
|
onRemove: function (map) {
|
|
L.GridLayer.prototype.onRemove.call(this, map);
|
|
map._container.removeChild(this._mutantContainer);
|
|
this._mutantContainer = undefined;
|
|
|
|
google.maps.event.clearListeners(map, 'idle');
|
|
google.maps.event.clearListeners(this._mutant, 'idle');
|
|
map.off('viewreset', this._reset, this);
|
|
map.off('move', this._update, this);
|
|
map.off('zoomend', this._handleZoomAnim, this);
|
|
map.off('resize', this._resize, this);
|
|
|
|
if (map._controlCorners) {
|
|
map._controlCorners.bottomright.style.marginBottom = '0em';
|
|
map._controlCorners.bottomleft.style.marginBottom = '0em';
|
|
}
|
|
},
|
|
|
|
getAttribution: function () {
|
|
return this.options.attribution;
|
|
},
|
|
|
|
setOpacity: function (opacity) {
|
|
this.options.opacity = opacity;
|
|
if (opacity < 1) {
|
|
L.DomUtil.setOpacity(this._mutantContainer, opacity);
|
|
}
|
|
},
|
|
|
|
setElementSize: function (e, size) {
|
|
e.style.width = size.x + 'px';
|
|
e.style.height = size.y + 'px';
|
|
},
|
|
|
|
|
|
addGoogleLayer: function (googleLayerName, options) {
|
|
if (!this._subLayers) this._subLayers = {};
|
|
return this._GAPIPromise.then(function () {
|
|
var Constructor = google.maps[googleLayerName];
|
|
var googleLayer = new Constructor(options);
|
|
googleLayer.setMap(this._mutant);
|
|
this._subLayers[googleLayerName] = googleLayer;
|
|
return googleLayer;
|
|
}.bind(this));
|
|
},
|
|
|
|
removeGoogleLayer: function (googleLayerName) {
|
|
var googleLayer = this._subLayers && this._subLayers[googleLayerName];
|
|
if (!googleLayer) return;
|
|
|
|
googleLayer.setMap(null);
|
|
delete this._subLayers[googleLayerName];
|
|
},
|
|
|
|
|
|
_initMutantContainer: function () {
|
|
if (!this._mutantContainer) {
|
|
this._mutantContainer = L.DomUtil.create('div', 'leaflet-google-mutant leaflet-top leaflet-left');
|
|
this._mutantContainer.id = '_MutantContainer_' + L.Util.stamp(this._mutantContainer);
|
|
this._mutantContainer.style.zIndex = '800'; //leaflet map pane at 400, controls at 1000
|
|
this._mutantContainer.style.pointerEvents = 'none';
|
|
|
|
this._map.getContainer().appendChild(this._mutantContainer);
|
|
}
|
|
|
|
this.setOpacity(this.options.opacity);
|
|
this.setElementSize(this._mutantContainer, this._map.getSize());
|
|
|
|
this._attachObserver(this._mutantContainer);
|
|
},
|
|
|
|
_initMutant: function () {
|
|
if (!this._ready || !this._mutantContainer) return;
|
|
this._mutantCenter = new google.maps.LatLng(0, 0);
|
|
|
|
var map = new google.maps.Map(this._mutantContainer, {
|
|
center: this._mutantCenter,
|
|
zoom: 0,
|
|
tilt: 0,
|
|
mapTypeId: this.options.type,
|
|
disableDefaultUI: true,
|
|
keyboardShortcuts: false,
|
|
draggable: false,
|
|
disableDoubleClickZoom: true,
|
|
scrollwheel: false,
|
|
streetViewControl: false,
|
|
styles: this.options.styles || {},
|
|
backgroundColor: 'transparent'
|
|
});
|
|
|
|
this._mutant = map;
|
|
|
|
google.maps.event.addListenerOnce(map, 'idle', function () {
|
|
var nodes = this._mutantContainer.querySelectorAll('a');
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
nodes[i].style.pointerEvents = 'auto';
|
|
}
|
|
}.bind(this));
|
|
|
|
// 🍂event spawned
|
|
// Fired when the mutant has been created.
|
|
this.fire('spawned', {mapObject: map});
|
|
},
|
|
|
|
_attachObserver: function _attachObserver (node) {
|
|
// console.log('Gonna observe', node);
|
|
|
|
var observer = new MutationObserver(this._onMutations.bind(this));
|
|
|
|
// pass in the target node, as well as the observer options
|
|
observer.observe(node, { childList: true, subtree: true });
|
|
},
|
|
|
|
_onMutations: function _onMutations (mutations) {
|
|
for (var i = 0; i < mutations.length; ++i) {
|
|
var mutation = mutations[i];
|
|
for (var j = 0; j < mutation.addedNodes.length; ++j) {
|
|
var node = mutation.addedNodes[j];
|
|
|
|
if (node instanceof HTMLImageElement) {
|
|
this._onMutatedImage(node);
|
|
} else if (node instanceof HTMLElement) {
|
|
Array.prototype.forEach.call(
|
|
node.querySelectorAll('img'),
|
|
this._boundOnMutatedImage
|
|
);
|
|
|
|
// Check for, and remove, the "Sorry, we have no imagery here"
|
|
// empty <div>s. The [style*="text-align: center"] selector
|
|
// avoids matching the attribution notice.
|
|
// This empty div doesn't have a reference to the tile
|
|
// coordinates, so it's not possible to mark the tile as
|
|
// failed.
|
|
Array.prototype.forEach.call(
|
|
node.querySelectorAll('div[draggable=false][style*="text-align: center"]'),
|
|
L.DomUtil.remove
|
|
)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Only images which 'src' attrib match this will be considered for moving around.
|
|
// Looks like some kind of string-based protobuf, maybe??
|
|
// Only the roads (and terrain, and vector-based stuff) match this pattern
|
|
_roadRegexp: /!1i(\d+)!2i(\d+)!3i(\d+)!/,
|
|
|
|
// On the other hand, raster imagery matches this other pattern
|
|
_satRegexp: /x=(\d+)&y=(\d+)&z=(\d+)/,
|
|
|
|
// On small viewports, when zooming in/out, a static image is requested
|
|
// This will not be moved around, just removed from the DOM.
|
|
_staticRegExp: /StaticMapService\.GetMapImage/,
|
|
|
|
_onMutatedImage: function _onMutatedImage (imgNode) {
|
|
// if (imgNode.src) {
|
|
// console.log('caught mutated image: ', imgNode.src);
|
|
// }
|
|
|
|
var coords;
|
|
var match = imgNode.src.match(this._roadRegexp);
|
|
var sublayer = 0;
|
|
|
|
if (match) {
|
|
coords = {
|
|
z: match[1],
|
|
x: match[2],
|
|
y: match[3]
|
|
};
|
|
if (this._imagesPerTile > 1) {
|
|
imgNode.style.zIndex = 1;
|
|
sublayer = 1;
|
|
}
|
|
} else {
|
|
match = imgNode.src.match(this._satRegexp);
|
|
if (match) {
|
|
coords = {
|
|
x: match[1],
|
|
y: match[2],
|
|
z: match[3]
|
|
};
|
|
}
|
|
// imgNode.style.zIndex = 0;
|
|
sublayer = 0;
|
|
}
|
|
|
|
if (coords) {
|
|
var tileKey = this._tileCoordsToKey(coords);
|
|
imgNode.style.position = 'absolute';
|
|
imgNode.style.visibility = 'hidden';
|
|
|
|
var key = tileKey + '/' + sublayer;
|
|
// console.log('mutation for tile', key)
|
|
//store img so it can also be used in subsequent tile requests
|
|
this._freshTiles[key] = imgNode;
|
|
|
|
if (key in this._tileCallbacks && this._tileCallbacks[key]) {
|
|
// console.log('Fullfilling callback ', key);
|
|
//fullfill most recent tileCallback because there maybe callbacks that will never get a
|
|
//corresponding mutation (because map moved to quickly...)
|
|
this._tileCallbacks[key].pop()(imgNode);
|
|
if (!this._tileCallbacks[key].length) { delete this._tileCallbacks[key]; }
|
|
} else {
|
|
if (this._tiles[tileKey]) {
|
|
//we already have a tile in this position (mutation is probably a google layer being added)
|
|
//replace it
|
|
var c = this._tiles[tileKey].el;
|
|
var oldImg = (sublayer === 0) ? c.firstChild : c.firstChild.nextSibling;
|
|
var cloneImgNode = this._clone(imgNode);
|
|
c.replaceChild(cloneImgNode, oldImg);
|
|
}
|
|
}
|
|
} else if (imgNode.src.match(this._staticRegExp)) {
|
|
imgNode.style.visibility = 'hidden';
|
|
}
|
|
},
|
|
|
|
|
|
createTile: function (coords, done) {
|
|
var key = this._tileCoordsToKey(coords);
|
|
|
|
var tileContainer = L.DomUtil.create('div');
|
|
tileContainer.dataset.pending = this._imagesPerTile;
|
|
done = done.bind(this, null, tileContainer);
|
|
|
|
for (var i = 0; i < this._imagesPerTile; i++) {
|
|
var key2 = key + '/' + i;
|
|
if (key2 in this._freshTiles) {
|
|
var imgNode = this._freshTiles[key2];
|
|
tileContainer.appendChild(this._clone(imgNode));
|
|
tileContainer.dataset.pending--;
|
|
// console.log('Got ', key2, ' from _freshTiles');
|
|
} else {
|
|
this._tileCallbacks[key2] = this._tileCallbacks[key2] || [];
|
|
this._tileCallbacks[key2].push( (function (c/*, k2*/) {
|
|
return function (imgNode) {
|
|
c.appendChild(this._clone(imgNode));
|
|
c.dataset.pending--;
|
|
if (!parseInt(c.dataset.pending)) { done(); }
|
|
// console.log('Sent ', k2, ' to _tileCallbacks, still ', c.dataset.pending, ' images to go');
|
|
}.bind(this);
|
|
}.bind(this))(tileContainer/*, key2*/) );
|
|
}
|
|
}
|
|
|
|
if (!parseInt(tileContainer.dataset.pending)) {
|
|
L.Util.requestAnimFrame(done);
|
|
}
|
|
return tileContainer;
|
|
},
|
|
|
|
_clone: function (imgNode) {
|
|
var clonedImgNode = imgNode.cloneNode(true);
|
|
clonedImgNode.style.visibility = 'visible';
|
|
return clonedImgNode;
|
|
},
|
|
|
|
_checkZoomLevels: function () {
|
|
//setting the zoom level on the Google map may result in a different zoom level than the one requested
|
|
//(it won't go beyond the level for which they have data).
|
|
var zoomLevel = this._map.getZoom();
|
|
var gMapZoomLevel = this._mutant.getZoom();
|
|
if (!zoomLevel || !gMapZoomLevel) return;
|
|
|
|
|
|
if ((gMapZoomLevel !== zoomLevel) || //zoom levels are out of sync, Google doesn't have data
|
|
(gMapZoomLevel > this.options.maxNativeZoom)) { //at current location, Google does have data (contrary to maxNativeZoom)
|
|
//Update maxNativeZoom
|
|
this._setMaxNativeZoom(gMapZoomLevel);
|
|
}
|
|
},
|
|
|
|
_setMaxNativeZoom: function (zoomLevel) {
|
|
if (zoomLevel != this.options.maxNativeZoom) {
|
|
this.options.maxNativeZoom = zoomLevel;
|
|
this._resetView();
|
|
}
|
|
},
|
|
|
|
_reset: function () {
|
|
this._initContainer();
|
|
},
|
|
|
|
_update: function () {
|
|
// zoom level check needs to happen before super's implementation (tile addition/creation)
|
|
// otherwise tiles may be missed if maxNativeZoom is not yet correctly determined
|
|
if (this._mutant) {
|
|
var center = this._map.getCenter();
|
|
var _center = new google.maps.LatLng(center.lat, center.lng);
|
|
|
|
this._mutant.setCenter(_center);
|
|
var zoom = this._map.getZoom();
|
|
var fractionalLevel = zoom !== Math.round(zoom);
|
|
var mutantZoom = this._mutant.getZoom();
|
|
|
|
//ignore fractional zoom levels
|
|
if (!fractionalLevel && (zoom != mutantZoom)) {
|
|
this._mutant.setZoom(zoom);
|
|
|
|
if (this._mutantIsReady) this._checkZoomLevels();
|
|
//else zoom level check will be done later by 'idle' handler
|
|
}
|
|
}
|
|
|
|
L.GridLayer.prototype._update.call(this);
|
|
},
|
|
|
|
_resize: function () {
|
|
var size = this._map.getSize();
|
|
if (this._mutantContainer.style.width === size.x &&
|
|
this._mutantContainer.style.height === size.y)
|
|
return;
|
|
this.setElementSize(this._mutantContainer, size);
|
|
if (!this._mutant) return;
|
|
google.maps.event.trigger(this._mutant, 'resize');
|
|
},
|
|
|
|
_handleZoomAnim: function () {
|
|
if (!this._mutant) return;
|
|
var center = this._map.getCenter();
|
|
var _center = new google.maps.LatLng(center.lat, center.lng);
|
|
|
|
this._mutant.setCenter(_center);
|
|
this._mutant.setZoom(Math.round(this._map.getZoom()));
|
|
},
|
|
|
|
// Agressively prune _freshtiles when a tile with the same key is removed,
|
|
// this prevents a problem where Leaflet keeps a loaded tile longer than
|
|
// GMaps, so that GMaps makes two requests but Leaflet only consumes one,
|
|
// polluting _freshTiles with stale data.
|
|
_removeTile: function (key) {
|
|
if (!this._mutant) return;
|
|
|
|
//give time for animations to finish before checking it tile should be pruned
|
|
setTimeout(this._pruneTile.bind(this, key), 1000);
|
|
|
|
|
|
return L.GridLayer.prototype._removeTile.call(this, key);
|
|
},
|
|
|
|
_pruneTile: function (key) {
|
|
var gZoom = this._mutant.getZoom();
|
|
var tileZoom = key.split(':')[2];
|
|
var googleBounds = this._mutant.getBounds();
|
|
var sw = googleBounds.getSouthWest();
|
|
var ne = googleBounds.getNorthEast();
|
|
var gMapBounds = L.latLngBounds([[sw.lat(), sw.lng()], [ne.lat(), ne.lng()]]);
|
|
|
|
for (var i=0; i<this._imagesPerTile; i++) {
|
|
var key2 = key + '/' + i;
|
|
if (key2 in this._freshTiles) {
|
|
var tileBounds = this._map && this._keyToBounds(key);
|
|
var stillVisible = this._map && tileBounds.overlaps(gMapBounds) && (tileZoom == gZoom);
|
|
|
|
if (!stillVisible) delete this._freshTiles[key2];
|
|
// console.log('Prunning of ', key, (!stillVisible))
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// 🍂factory gridLayer.googleMutant(options)
|
|
// Returns a new `GridLayer.GoogleMutant` given its options
|
|
L.gridLayer.googleMutant = function (options) {
|
|
return new L.GridLayer.GoogleMutant(options);
|
|
};
|