Initial commit

This commit is contained in:
Victor Golovanenko 2022-05-26 09:53:13 +03:00
commit a3862a975c
Signed by: drygdryg
GPG Key ID: 50E0F568281DB835
53 changed files with 61814 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

3
MANIFEST.in Normal file
View File

@ -0,0 +1,3 @@
include README.md
recursive-include netbox_device_map/templates *
recursive-include netbox_device_map/static *

85
README.md Normal file
View File

@ -0,0 +1,85 @@
# NetBox device map
A simple device map plugin with filtering criteria for NetBox
![Map screenshot](docs/images/screenshot_map.png)
![Map filters screenshot](docs/images/screenshot_map_filters.png)
## Installation
1. Download plugin distribution from releases.
2. If your NetBox installation uses virtualenv, activate it like this:
```
source /opt/netbox/venv/bin/activate
```
3. Install the plugin from the distribution:
```
sudo pip install /path/to/netbox-plugin-device-map.tar.gz
```
4. Add plugin to `local_requirements.txt`:
To ensure plugin is automatically re-installed during future NetBox upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory
and list the `nextbox-plugin-device-map` package:
```
echo "/path/to/netbox-plugin-device-map.tar.gz" | sudo tee -a /opt/netbox/local_requirements.txt
```
5. Collect static files:
```
sudo python /opt/netbox/netbox/manage.py collectstatic
```
6. To enable plugin, add the plugin's name to the PLUGINS list in `configuration.py` (it's usually located in `/opt/netbox/netbox/netbox/`) like so:
```
PLUGINS = [
'netbox_device_map'
]
```
7. Restart NetBox WSGI service to apply changes:
```
sudo systemctl restart netbox
```
## Configuration
You can customize plugin behavior according to your needs. For example, change the custom field that contains device coordinates or install custom map tiles.
Update PLUGINS_CONFIG parameter in the `configuration.py` like this:
```python
PLUGINS_CONFIG = {
'netbox_device_map': {
'device_geolocation_cf': 'coordinates',
'cpe_device_role': 'CPE',
'geomap_settings': {
'attribution': '<a href="https://osm.yourdomain.net">Your company</a> | © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
'tiles': {
'url_template': 'https://osm.yourdomain.net/hot/{z}/{x}/{y}.png',
'options': {
'maxZoom': 19
}
}
}
}
}
```
### Settings
| Setting | Default value | Description |
|-----------------------|---------------|-------------------------------------------------------------------------------------------------------------|
| device_geolocation_cf | `geolocation` | NetBox custom field for storing geographical location of devices (in the `"<latitude>,<longitude>"` format) |
| cpe_device_role | `CPE` | Name of the NetBox device role that contains CPE devices |
| geomap_settings | … | Geographical map settings |
#### Geographical map settings
| Setting | Default value | Description |
|-------------|------------------------------------------------------------------------|-------------------------------------------------------|
| attribution | `Data by &copy; <a href="https://openstreetmap.org">OpenStreetMap</a>` | Attribution text in the lower right corner of the map |
| crs | `EPSG3857` | Coordinate reference system |
| tiles | … | Tiles layer settings |
Custom tiles layer settings:
| Setting | Example value | Description |
|--------------|-----------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| url_template | `https://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png` | `{s}` means one of the available subdomains (used sequentially to help with browser parallel requests per domain limitation; subdomain values are specified in options; a, b or c by default, can be omitted), `{z}` — zoom level, `{x}` and `{y}` — tile coordinates. `{r}` can be used to add "@2x" to the URL to load retina tiles. |
| options | `{'subdomains' : ['a', 'b', 'c'], 'minZoom': 0, 'maxZoom': 18}` | [Leaflet TileLayer](https://leafletjs.com/SlavaUkraini/reference.html#tilelayer) options |
## Acknowledgements
- [Leaflet](https://leafletjs.com/)
### Leaflet plugins
- [leaflet.fullscreen](https://github.com/brunob/leaflet.fullscreen)
- [Leaflet-SVGIcon](https://github.com/iatkin/leaflet-svgicon)
- [leaflet-sidebar](https://github.com/Turbo87/leaflet-sidebar)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1011 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -0,0 +1,27 @@
from extras.plugins import PluginConfig
class DeviceMapConfig(PluginConfig):
name = 'netbox_device_map'
verbose_name = 'Device map'
version = '0.1'
author = 'Victor Golovanenko'
author_email = 'drygdryg2014@yandex.com'
base_url = 'device-map'
default_settings = {
'device_geolocation_cf': 'geolocation',
'cpe_device_role': 'CPE',
'geomap_settings': {
'attribution': 'Data by &copy; <a href="https://openstreetmap.org">OpenStreetMap</a>',
'crs': 'EPSG3857',
'tiles': {
'url_template': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
'options': {
'subdomains': 'abc',
}
}
}
}
config = DeviceMapConfig

View File

@ -0,0 +1,35 @@
from django import forms
from dcim.models import DeviceRole, Device
from ipam.models import VLANGroup, VLAN
from utilities.forms import BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField
class DeviceMapFilterForm(BootstrapMixin, forms.Form):
vlan_group = DynamicModelChoiceField(
queryset=VLANGroup.objects.all(),
required=False,
label="VLAN group",
help_text="VLAN group for VLAN selection"
)
vlan = DynamicModelChoiceField(
queryset=VLAN.objects.all(),
label="VLAN",
help_text="Filter devices by VLAN attached to any device interface",
query_params={"group_id": "$vlan_group"}
)
device_roles = DynamicModelMultipleChoiceField(
queryset=DeviceRole.objects.all(),
required=False,
label="Device roles",
help_text="Display devices of only the specified device roles"
)
calculate_connections = forms.BooleanField(
required=False,
label="Calculate connections between devices",
initial=True
)
class ConnectedCpeForm(forms.Form):
vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False)

View File

@ -0,0 +1,39 @@
from dcim.models import Device
from .settings import plugin_settings
from .helpers import get_connected_devices, LatLon
geomap_settings = plugin_settings['geomap_settings']
CPE_DEVICE_ROLE_NAME = plugin_settings['cpe_device_role']
def configure_leaflet_map(map_id: str, devices: dict[Device, LatLon], calculate_connections=True) -> dict:
"""Generate Leaflet map of devices and the connections between them.
:param map_id: initialize the map on the div with this id
:param devices: list of target devices to display on the map
:param calculate_connections: calculate connections between devices
"""
device_id_to_latlon = {device.id: position for device, position in devices.items()}
map_config = dict(**geomap_settings, map_id=map_id)
markers: list[dict] = []
connections: set[frozenset[LatLon, LatLon]] = set()
for device, position in devices.items():
markers.append(dict(
position=position,
icon=device.device_role.slug,
device=dict(
id=device.id,
name=device.name,
url=device.get_absolute_url(),
role=device.device_role.name
)
))
if calculate_connections:
for peer_device_id in get_connected_devices(device).values_list('id', flat=True).order_by():
if peer_position := device_id_to_latlon.get(peer_device_id):
connections.add(frozenset((position, peer_position)))
map_config.update(markers=markers, connections=[tuple(c) for c in connections])
return map_config

View File

@ -0,0 +1,32 @@
from dcim.models import Device
from django.db.models import QuerySet, Q
from ipam.models import VLAN
from .settings import plugin_settings
LOCATION_CF_NAME = plugin_settings['device_geolocation_cf']
LatLon = tuple[float, float]
def get_device_location(device: Device) -> LatLon | None:
"""Extract device geolocation from special custom field"""
if location_cf := device.custom_field_data.get(LOCATION_CF_NAME):
return tuple(map(float, location_cf.replace(' ', '').split(',', maxsplit=1)))
def get_connected_devices(device: Device, vlan: VLAN = None) -> QuerySet[Device]:
"""Get list of connected devices to the specified device.
If the vlan is specified, return only devices connected to the interfaces of the specified device
containing the specified VLAN"""
included_interfaces = device.interfaces.all()
if vlan is not None:
included_interfaces = included_interfaces.filter(Q(untagged_vlan=vlan) | Q(tagged_vlans=vlan))
return Device.objects.filter(interfaces___link_peer_id__in=included_interfaces)
def are_devices_connected(device_a: Device, device_b: Device) -> bool:
"""Determines whether devices are connected to each other by a direct connection"""
return bool(
Device.objects.filter(interfaces___link_peer_id__in=device_a.interfaces.all(), id=device_b.id).values('pk')
)

View File

@ -0,0 +1,9 @@
from extras.plugins import PluginMenuItem
menu_items = (
PluginMenuItem(
link='plugins:netbox_device_map:map',
link_text='Device map',
),
)

View File

@ -0,0 +1,7 @@
from django.conf import settings
from . import config
# Overlay custom settings over default settings
plugin_settings = config.default_settings | settings.PLUGINS_CONFIG[config.name]
plugin_settings['geomap_settings'] = config.default_settings['geomap_settings'] | plugin_settings['geomap_settings']

View File

@ -0,0 +1,119 @@
let default_marker_icon = {
iconSize: [22, 33],
shadowEnable: true,
shadowOpacity: 0.25,
shadowAngle: 27,
shadowLength: 0.64,
shadowBlur: 1.5
}
let marker_icon_configs = {
'access-switch': Object.assign({color: "#2da652"}, default_marker_icon),
'core-switch': Object.assign({color: "#d30b0b"}, default_marker_icon),
'distribution-switch': Object.assign({color: "#277fca"}, default_marker_icon),
olt: Object.assign({color: "#c5ba26"}, default_marker_icon),
router: Object.assign({color: "#26A69A"}, default_marker_icon),
wifi: Object.assign({color: "#8111ea"}, default_marker_icon)
}
const map_data = JSON.parse(document.getElementById('map-data').textContent)
let geomap = L.map(map_data.map_id,
{
crs: L.CRS[map_data.crs],
layers: [L.tileLayer(map_data.tiles.url_template, map_data.tiles.options)],
fullscreenControl: true,
fullscreenControlOptions: {position: 'topright'}
}
)
geomap.attributionControl.setPrefix(`<a href="https://leafletjs.com" title="A JavaScript library for interactive maps">Leaflet</a>`)
geomap.attributionControl.addAttribution(map_data.attribution)
let sidebar = L.control.sidebar('map-sidebar', {
closeButton: true,
position: 'left'
})
geomap.addControl(sidebar);
let bounds = new L.LatLngBounds()
// Preparing to place markers with the same coordinates in clusters
let markers = {}
map_data.markers.forEach(function(entry) {
let key = entry.position.toString()
if (key in markers) {
markers[key].push(entry)
} else {
markers[key] = [entry]
}
})
for (let key in markers) {
const marker_parent_layer = markers[key].length > 1 ? L.markerClusterGroup() : geomap;
for (let marker_data of markers[key]) {
let iconOptions = {}
if (marker_data.icon && marker_data.icon in marker_icon_configs) {
iconOptions = marker_icon_configs[marker_data.icon]
} else {
iconOptions = default_marker_icon
}
let markerObj = L.marker(marker_data.position, {icon: L.divIcon.svgIcon(iconOptions), device: marker_data.device})
.bindTooltip(`${marker_data.device.name}<br><span class="text-muted">${marker_data.device.role}</span>`)
markerObj.on('click', function (event) {
let device = event.target.options.device
if (sidebar.isVisible() && (sidebar.displayed_device === device.id)) {
sidebar.displayed_device = undefined
sidebar.hide()
} else {
sidebar.displayed_device = device.id
document.querySelector('.sidebar-device-name').innerHTML = `<a href="${device.url}" target="_blank">${device.name}</a>`
document.querySelector('.sidebar-device-role').innerHTML = device.role
sidebar.show()
fetch(`connected-cpe/${device.id}?vlan=${map_data.vlan}`)
.then(response => response.json()).then(
function (response) {
if (response.status === true) {
document.querySelector('.sidebar-device-type').innerHTML = response.device_type
let cpe_list = document.querySelector('.sidebar-cpe-list')
cpe_list.innerHTML = ""
if (response.cpe_devices?.length) {
cpe_list.innerHTML = `<div class="mb-2">Connected CPEs in the selected VLAN:</div>`
let ul = document.createElement('ul')
ul.setAttribute('class', 'mb-0')
cpe_list.appendChild(ul)
for (let cpe_device of response.cpe_devices) {
let li = document.createElement('li');
li.innerHTML = `<a href="${cpe_device.url}" target="_blank">${cpe_device.name}</a>
<span class="separator">·</span>
<span class="text-muted">${cpe_device.comments}</span>`
ul.appendChild(li)
}
} else {
cpe_list.innerHTML = "<i>There are no connected CPEs in the selected VLAN</i>"
}
}
}
)
}
})
bounds.extend(marker_data.position)
marker_parent_layer.addLayer(markerObj)
}
if (markers[key].length > 1) {
geomap.addLayer(marker_parent_layer)
}
}
const normalLineStyle = {weight: 3, color: '#3388ff'}
const boldLineStyle ={weight: 5, color:'#0c10ff'};
for (let connection of map_data.connections) {
let line = L.polyline(connection, normalLineStyle).addTo(geomap)
line.on('mouseover', function () {this.setStyle(boldLineStyle); this.bringToFront()})
line.on('mouseout', function () {this.setStyle(normalLineStyle)})
}
if (bounds.isValid()) {
geomap.fitBounds(bounds)
} else {
geomap.fitWorld()
}

View File

@ -0,0 +1,267 @@
//Leaflet-SVGIcon
//SVG icon for any marker class
//Ilya Atkin
//ilya.atkin@unh.edu
L.DivIcon.SVGIcon = L.DivIcon.extend({
options: {
"className": "svg-icon",
"circleAnchor": null, //defaults to [iconSize.x/2, iconSize.x/2]
"circleColor": null, //defaults to color
"circleFillColor": "rgb(255,255,255)",
"circleFillOpacity": null, //default to opacity
"circleImageAnchor": null, //defaults to [(iconSize.x - circleImageSize.x)/2, (iconSize.x - circleImageSize.x)/2]
"circleImagePath": null, //no default, preference over circleText
"circleImageSize": null, //defaults to [iconSize.x/4, iconSize.x/4] if circleImage is supplied
"circleOpacity": null, // defaults to opacity
"circleRatio": 0.5,
"circleText": "",
"circleWeight": null, //defaults to weight
"color": "rgb(0,102,255)",
"fillColor": null, // defaults to color
"fillOpacity": 0.4,
"fontColor": "rgb(0, 0, 0)",
"fontOpacity": "1",
"fontSize": null, // defaults to iconSize.x/4
"fontWeight": "normal",
"iconAnchor": null, //defaults to [iconSize.x/2, iconSize.y] (point tip)
"iconSize": L.point(32,48),
"opacity": 1,
"popupAnchor": null,
"shadowAngle": 45,
"shadowBlur": 1,
"shadowColor": "rgb(0,0,10)",
"shadowEnable": false,
"shadowLength": .75,
"shadowOpacity": 0.5,
"shadowTranslate": L.point(0,0),
"weight": 2
},
initialize: function(options) {
options = L.Util.setOptions(this, options)
//iconSize needs to be converted to a Point object if it is not passed as one
options.iconSize = L.point(options.iconSize)
//in addition to setting option dependant defaults, Point-based options are converted to Point objects
if (!options.circleAnchor) {
options.circleAnchor = L.point(Number(options.iconSize.x)/2, Number(options.iconSize.x)/2)
}
else {
options.circleAnchor = L.point(options.circleAnchor)
}
if (!options.circleColor) {
options.circleColor = options.color
}
if (!options.circleFillOpacity) {
options.circleFillOpacity = options.opacity
}
if (!options.circleOpacity) {
options.circleOpacity = options.opacity
}
if (!options.circleWeight) {
options.circleWeight = options.weight
}
if (!options.fillColor) {
options.fillColor = options.color
}
if (!options.fontSize) {
options.fontSize = Number(options.iconSize.x/4)
}
if (!options.iconAnchor) {
options.iconAnchor = L.point(Number(options.iconSize.x)/2, Number(options.iconSize.y))
}
else {
options.iconAnchor = L.point(options.iconAnchor)
}
if (!options.popupAnchor) {
options.popupAnchor = L.point(0, (-0.75)*(options.iconSize.y))
}
else {
options.popupAnchor = L.point(options.popupAnchor)
}
if (options.circleImagePath && !options.circleImageSize) {
options.circleImageSize = L.point(Number(options.iconSize.x)/4, Number(options.iconSize.x)/4)
}
else {
options.circleImageSize = L.point(options.circleImageSize)
}
if (options.circleImagePath && !options.circleImageAnchor) {
options.circleImageAnchor = L.point(
(Number(options.iconSize.x) - Number(options.circleImageSize.x))/2,
(Number(options.iconSize.x) - Number(options.circleImageSize.y))/2
)
}
else {
options.circleImageAnchor = L.point(options.circleImageAnchor)
}
options.html = this._createSVG()
},
_createCircle: function() {
var cx = Number(this.options.circleAnchor.x)
var cy = Number(this.options.circleAnchor.y)
var radius = this.options.iconSize.x/2 * Number(this.options.circleRatio)
var fill = this.options.circleFillColor
var fillOpacity = this.options.circleFillOpacity
var stroke = this.options.circleColor
var strokeOpacity = this.options.circleOpacity
var strokeWidth = this.options.circleWeight
var className = this.options.className + "-circle"
var circle = '<circle class="' + className + '" cx="' + cx + '" cy="' + cy + '" r="' + radius +
'" fill="' + fill + '" fill-opacity="'+ fillOpacity +
'" stroke="' + stroke + '" stroke-opacity=' + strokeOpacity + '" stroke-width="' + strokeWidth + '"/>'
return circle
},
_createCircleImage: function() {
var x = this.options.circleImageAnchor.x
var y = this.options.circleImageAnchor.y
var height = this.options.circleImageSize.y
var width = this.options.circleImageSize.x
var href = this.options.circleImagePath
var image = '<image x="' + x + '" y="' + y + '" height="' + height + '" width="' + width + '" href="' + href + '"</image>'
return image
},
_createPathDescription: function() {
var height = Number(this.options.iconSize.y)
var width = Number(this.options.iconSize.x)
var weight = Number(this.options.weight)
var margin = weight / 2
var startPoint = "M " + margin + " " + (width/2) + " "
var leftLine = "L " + (width/2) + " " + (height - weight) + " "
var rightLine = "L " + (width - margin) + " " + (width/2) + " "
var arc = "A " + (width/4) + " " + (width/4) + " 0 0 0 " + margin + " " + (width/2) + " Z"
var d = startPoint + leftLine + rightLine + arc
return d
},
_createPath: function() {
var pathDescription = this._createPathDescription()
var strokeWidth = this.options.weight
var stroke = this.options.color
var strokeOpacity = this.options.opacity
var fill = this.options.fillColor
var fillOpacity = this.options.fillOpacity
var className = this.options.className + "-path"
var path = '<path class="' + className + '" d="' + pathDescription +
'" stroke-width="' + strokeWidth + '" stroke="' + stroke + '" stroke-opacity="' + strokeOpacity +
'" fill="' + fill + '" fill-opacity="' + fillOpacity + '"/>'
return path
},
_createShadow: function() {
var pathDescription = this._createPathDescription()
var strokeWidth = this.options.weight
var stroke = this.options.shadowColor
var fill = this.options.shadowColor
var className = this.options.className + "-shadow"
var origin = (this.options.iconSize.x / 2) + "px " + (this.options.iconSize.y) + "px"
var rotation = this.options.shadowAngle
var height = this.options.shadowLength
var opacity = this.options.shadowOpacity
var blur = this.options.shadowBlur
var translate = this.options.shadowTranslate.x + "px, " + this.options.shadowTranslate.y + "px"
var blurFilter = "<filter id='iconShadowBlur'><feGaussianBlur in='SourceGraphic' stdDeviation='" + blur + "'/></filter>"
var shadow = '<path filter="url(#iconShadowBlur") class="' + className + '" d="' + pathDescription +
'" fill="' + fill + '" stroke-width="' + strokeWidth + '" stroke="' + stroke +
'" style="opacity: ' + opacity + '; ' + 'transform-origin: ' + origin +'; transform: rotate(' + rotation + 'deg) translate(' + translate + ') scale(1, '+ height +')' +
'"/>'
return blurFilter+shadow
},
_createSVG: function() {
var path = this._createPath()
var circle = this._createCircle()
var shadow = this.options.shadowEnable ? this._createShadow() : ""
var innerCircle = this.options.circleImagePath ? this._createCircleImage() : this._createText()
var className = this.options.className + "-svg"
var width = this.options.iconSize.x
var height = this.options.iconSize.y
if (this.options.shadowEnable) {
width += this.options.iconSize.y * this.options.shadowLength - (this.options.iconSize.x / 2)
width = Math.max(width, 32)
height += this.options.iconSize.y * this.options.shadowLength
}
var style = "width:" + width + "px; height:" + height
var svg = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="' + className + '" style="' + style + '">' + shadow + path + circle + innerCircle + '</svg>'
return svg
},
_createText: function() {
var fontSize = this.options.fontSize + "px"
var fontWeight = this.options.fontWeight
var lineHeight = Number(this.options.fontSize)
var x = this.options.circleAnchor.x
var y = this.options.circleAnchor.y + (lineHeight * 0.35) //35% was found experimentally
var circleText = this.options.circleText
var textColor = this.options.fontColor.replace("rgb(", "rgba(").replace(")", "," + this.options.fontOpacity + ")")
var text = '<text text-anchor="middle" x="' + x + '" y="' + y + '" style="font-size: ' + fontSize + '; font-weight: ' + fontWeight +'" fill="' + textColor + '">' + circleText + '</text>'
return text
}
})
L.divIcon.svgIcon = function(options) {
return new L.DivIcon.SVGIcon(options)
}
L.Marker.SVGMarker = L.Marker.extend({
options: {
"iconFactory": L.divIcon.svgIcon,
"iconOptions": {}
},
initialize: function(latlng, options) {
options = L.Util.setOptions(this, options)
options.icon = options.iconFactory(options.iconOptions)
this._latlng = latlng
},
onAdd: function(map) {
L.Marker.prototype.onAdd.call(this, map)
},
setStyle: function(style) {
if (this._icon) {
var svg = this._icon.children[0]
var iconBody = this._icon.children[0].children[0]
var iconCircle = this._icon.children[0].children[1]
if (style.color && !style.iconOptions) {
var stroke = style.color.replace("rgb","rgba").replace(")", ","+this.options.icon.options.opacity+")")
var fill = style.color.replace("rgb","rgba").replace(")", ","+this.options.icon.options.fillOpacity+")")
iconBody.setAttribute("stroke", stroke)
iconBody.setAttribute("fill", fill)
iconCircle.setAttribute("stroke", stroke)
this.options.icon.fillColor = fill
this.options.icon.color = stroke
this.options.icon.circleColor = stroke
}
if (style.opacity) {
this.setOpacity(style.opacity)
}
if (style.iconOptions) {
if (style.color) { style.iconOptions.color = style.color }
var iconOptions = L.Util.setOptions(this.options.icon, style.iconOptions)
this.setIcon(L.divIcon.svgIcon(iconOptions))
}
}
}
})
L.marker.svgMarker = function(latlng, options) {
return new L.Marker.SVGMarker(latlng, options)
}

View File

@ -0,0 +1,102 @@
.leaflet-sidebar {
position: absolute;
height: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
z-index: 2000; }
.leaflet-sidebar.left {
left: -500px;
transition: left 0.5s, width 0.5s;
padding-right: 0; }
.leaflet-sidebar.left.visible {
left: 0; }
.leaflet-sidebar.right {
right: -500px;
transition: right 0.5s, width 0.5s;
padding-left: 0; }
.leaflet-sidebar.right.visible {
right: 0; }
.leaflet-sidebar > .leaflet-control {
height: 100%;
width: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 8px 24px;
font-size: 1.1em;
background: white;
box-shadow: 0 1px 7px rgba(0, 0, 0, 0.65);
-webkit-border-radius: 4px;
border-radius: 4px; }
.leaflet-touch .leaflet-sidebar > .leaflet-control {
box-shadow: none;
border: 2px solid rgba(0, 0, 0, 0.2);
background-clip: padding-box; }
@media (max-width: 767px) {
.leaflet-sidebar {
width: 100%;
padding: 0; }
.leaflet-sidebar.left.visible ~ .leaflet-left {
left: 100%; }
.leaflet-sidebar.right.visible ~ .leaflet-right {
right: 100%; }
.leaflet-sidebar.left {
left: -100%; }
.leaflet-sidebar.left.visible {
left: 0; }
.leaflet-sidebar.right {
right: -100%; }
.leaflet-sidebar.right.visible {
right: 0; }
.leaflet-sidebar > .leaflet-control {
box-shadow: none;
-webkit-border-radius: 0;
border-radius: 0; }
.leaflet-touch .leaflet-sidebar > .leaflet-control {
border: 0; } }
@media (min-width: 768px) and (max-width: 991px) {
.leaflet-sidebar {
width: 305px; }
.leaflet-sidebar.left.visible ~ .leaflet-left {
left: 305px; }
.leaflet-sidebar.right.visible ~ .leaflet-right {
right: 305px; } }
@media (min-width: 992px) and (max-width: 1199px) {
.leaflet-sidebar {
width: 390px; }
.leaflet-sidebar.left.visible ~ .leaflet-left {
left: 390px; }
.leaflet-sidebar.right.visible ~ .leaflet-right {
right: 390px; } }
@media (min-width: 1200px) {
.leaflet-sidebar {
width: 460px; }
.leaflet-sidebar.left.visible ~ .leaflet-left {
left: 460px; }
.leaflet-sidebar.right.visible ~ .leaflet-right {
right: 460px; } }
.leaflet-sidebar .close {
position: absolute;
right: 20px;
top: 20px;
width: 31px;
height: 31px;
color: #333;
font-size: 25px;
line-height: 1em;
text-align: center;
background: white;
-webkit-border-radius: 16px;
border-radius: 16px;
cursor: pointer;
z-index: 1000; }
.leaflet-left {
transition: left 0.5s; }
.leaflet-right {
transition: right 0.5s; }

View File

@ -0,0 +1,202 @@
L.Control.Sidebar = L.Control.extend({
includes: L.Evented.prototype || L.Mixin.Events,
options: {
closeButton: true,
position: 'left',
autoPan: true,
},
initialize: function (placeholder, options) {
L.setOptions(this, options);
// Find content container
var content = this._contentContainer = L.DomUtil.get(placeholder);
// Remove the content container from its original parent
if(content.parentNode != undefined){
content.parentNode.removeChild(content);
}
var l = 'leaflet-';
// Create sidebar container
var container = this._container =
L.DomUtil.create('div', l + 'sidebar ' + this.options.position);
// Style and attach content container
L.DomUtil.addClass(content, l + 'control');
container.appendChild(content);
// Create close button and attach it if configured
if (this.options.closeButton) {
var close = this._closeButton =
L.DomUtil.create('a', 'close', container);
close.innerHTML = '&times;';
}
},
addTo: function (map) {
var container = this._container;
var content = this._contentContainer;
// Attach event to close button
if (this.options.closeButton) {
var close = this._closeButton;
L.DomEvent.on(close, 'click', this.hide, this);
}
L.DomEvent
.on(container, 'transitionend',
this._handleTransitionEvent, this)
.on(container, 'webkitTransitionEnd',
this._handleTransitionEvent, this);
// Attach sidebar container to controls container
var controlContainer = map._controlContainer;
controlContainer.insertBefore(container, controlContainer.firstChild);
this._map = map;
// Make sure we don't drag the map when we interact with the content
var stop = L.DomEvent.stopPropagation;
var fakeStop = L.DomEvent._fakeStop || stop;
L.DomEvent
.on(content, 'contextmenu', stop)
.on(content, 'click', fakeStop)
.on(content, 'mousedown', stop)
.on(content, 'touchstart', stop)
.on(content, 'dblclick', fakeStop)
.on(content, 'mousewheel', stop)
.on(content, 'wheel', stop)
.on(content, 'scroll', stop)
.on(content, 'MozMousePixelScroll', stop);
return this;
},
removeFrom: function (map) {
//if the control is visible, hide it before removing it.
this.hide();
var container = this._container;
var content = this._contentContainer;
// Remove sidebar container from controls container
var controlContainer = map._controlContainer;
controlContainer.removeChild(container);
//disassociate the map object
this._map = null;
// Unregister events to prevent memory leak
var stop = L.DomEvent.stopPropagation;
var fakeStop = L.DomEvent._fakeStop || stop;
L.DomEvent
.off(content, 'contextmenu', stop)
.off(content, 'click', fakeStop)
.off(content, 'mousedown', stop)
.off(content, 'touchstart', stop)
.off(content, 'dblclick', fakeStop)
.off(content, 'mousewheel', stop)
.off(content, 'wheel', stop)
.off(content, 'scroll', stop)
.off(content, 'MozMousePixelScroll', stop);
L.DomEvent
.off(container, 'transitionend',
this._handleTransitionEvent, this)
.off(container, 'webkitTransitionEnd',
this._handleTransitionEvent, this);
if (this._closeButton && this._close) {
var close = this._closeButton;
L.DomEvent.off(close, 'click', this.hide, this);
}
return this;
},
isVisible: function () {
return L.DomUtil.hasClass(this._container, 'visible');
},
show: function () {
if (!this.isVisible()) {
L.DomUtil.addClass(this._container, 'visible');
if (this.options.autoPan) {
this._map.panBy([-this.getOffset() / 2, 0], {
duration: 0.5
});
}
this.fire('show');
}
},
hide: function (e) {
if (this.isVisible()) {
L.DomUtil.removeClass(this._container, 'visible');
if (this.options.autoPan) {
this._map.panBy([this.getOffset() / 2, 0], {
duration: 0.5
});
}
this.fire('hide');
}
if(e) {
L.DomEvent.stopPropagation(e);
}
},
toggle: function () {
if (this.isVisible()) {
this.hide();
} else {
this.show();
}
},
getContainer: function () {
return this._contentContainer;
},
getCloseButton: function () {
return this._closeButton;
},
setContent: function (content) {
var container = this.getContainer();
if (typeof content === 'string') {
container.innerHTML = content;
} else {
// clean current content
while (container.firstChild) {
container.removeChild(container.firstChild);
}
container.appendChild(content);
}
return this;
},
getOffset: function () {
if (this.options.position === 'right') {
return -this._container.offsetWidth;
} else {
return this._container.offsetWidth;
}
},
_handleTransitionEvent: function (e) {
if (e.propertyName == 'left' || e.propertyName == 'right')
this.fire(this.isVisible() ? 'shown' : 'hidden');
}
});
L.control.sidebar = function (placeholder, options) {
return new L.Control.Sidebar(placeholder, options);
};

View File

@ -0,0 +1,10 @@
.fullscreen-icon { background-image: url(icon-fullscreen.svg); background-size:26px 52px; }
.fullscreen-icon.leaflet-fullscreen-on { background-position:0 -26px; }
.leaflet-touch .fullscreen-icon { background-position: 2px 2px; }
.leaflet-touch .fullscreen-icon.leaflet-fullscreen-on { background-position: 2px -24px; }
/* one selector per rule as explained here : http://www.sitepoint.com/html5-full-screen-api/ */
.leaflet-container:-webkit-full-screen { width: 100% !important; height: 100% !important; z-index: 99999; }
.leaflet-container:-ms-fullscreen { width: 100% !important; height: 100% !important; z-index: 99999; }
.leaflet-container:full-screen { width: 100% !important; height: 100% !important; z-index: 99999; }
.leaflet-container:fullscreen { width: 100% !important; height: 100% !important; z-index: 99999; }
.leaflet-pseudo-fullscreen { position: fixed !important; width: 100% !important; height: 100% !important; top: 0px !important; left: 0px !important; z-index: 99999; }

View File

@ -0,0 +1,341 @@
/*!
* Based on package 'screenfull'
* v5.2.0 - 2021-11-03
* (c) Sindre Sorhus; MIT License
* Added definition for using screenfull as an amd module
* Must be placed before the definition of leaflet.fullscreen
* as it is required by that
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define('screenfull', factory);
} else if (typeof module === 'object' && module.exports) {
module.exports.screenfull = factory();
} else {
// Save 'screenfull' into global window variable
root.screenfull = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {};
var fn = (function () {
var val;
var fnMap = [
[
'requestFullscreen',
'exitFullscreen',
'fullscreenElement',
'fullscreenEnabled',
'fullscreenchange',
'fullscreenerror'
],
// New WebKit
[
'webkitRequestFullscreen',
'webkitExitFullscreen',
'webkitFullscreenElement',
'webkitFullscreenEnabled',
'webkitfullscreenchange',
'webkitfullscreenerror'
],
// Old WebKit
[
'webkitRequestFullScreen',
'webkitCancelFullScreen',
'webkitCurrentFullScreenElement',
'webkitCancelFullScreen',
'webkitfullscreenchange',
'webkitfullscreenerror'
],
[
'mozRequestFullScreen',
'mozCancelFullScreen',
'mozFullScreenElement',
'mozFullScreenEnabled',
'mozfullscreenchange',
'mozfullscreenerror'
],
[
'msRequestFullscreen',
'msExitFullscreen',
'msFullscreenElement',
'msFullscreenEnabled',
'MSFullscreenChange',
'MSFullscreenError'
]
];
var i = 0;
var l = fnMap.length;
var ret = {};
for (; i < l; i++) {
val = fnMap[i];
if (val && val[1] in document) {
for (i = 0; i < val.length; i++) {
ret[fnMap[0][i]] = val[i];
}
return ret;
}
}
return false;
})();
var eventNameMap = {
change: fn.fullscreenchange,
error: fn.fullscreenerror
};
var screenfull = {
request: function (element, options) {
return new Promise(function (resolve, reject) {
var onFullScreenEntered = function () {
this.off('change', onFullScreenEntered);
resolve();
}.bind(this);
this.on('change', onFullScreenEntered);
element = element || document.documentElement;
var returnPromise = element[fn.requestFullscreen](options);
if (returnPromise instanceof Promise) {
returnPromise.then(onFullScreenEntered).catch(reject);
}
}.bind(this));
},
exit: function () {
return new Promise(function (resolve, reject) {
if (!this.isFullscreen) {
resolve();
return;
}
var onFullScreenExit = function () {
this.off('change', onFullScreenExit);
resolve();
}.bind(this);
this.on('change', onFullScreenExit);
var returnPromise = document[fn.exitFullscreen]();
if (returnPromise instanceof Promise) {
returnPromise.then(onFullScreenExit).catch(reject);
}
}.bind(this));
},
toggle: function (element, options) {
return this.isFullscreen ? this.exit() : this.request(element, options);
},
onchange: function (callback) {
this.on('change', callback);
},
onerror: function (callback) {
this.on('error', callback);
},
on: function (event, callback) {
var eventName = eventNameMap[event];
if (eventName) {
document.addEventListener(eventName, callback, false);
}
},
off: function (event, callback) {
var eventName = eventNameMap[event];
if (eventName) {
document.removeEventListener(eventName, callback, false);
}
},
raw: fn
};
if (!fn) {
return {isEnabled: false};
} else {
Object.defineProperties(screenfull, {
isFullscreen: {
get: function () {
return Boolean(document[fn.fullscreenElement]);
}
},
element: {
enumerable: true,
get: function () {
return document[fn.fullscreenElement];
}
},
isEnabled: {
enumerable: true,
get: function () {
// Coerce to boolean in case of old WebKit
return Boolean(document[fn.fullscreenEnabled]);
}
}
});
return screenfull;
}
}));
/*!
* leaflet.fullscreen
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// define an AMD module that requires 'leaflet' and 'screenfull'
// and resolve to an object containing leaflet and screenfull
define('leafletFullScreen', ['leaflet', 'screenfull'], factory);
} else if (typeof module === 'object' && module.exports) {
// define a CommonJS module that requires 'leaflet' and 'screenfull'
module.exports = factory(require('leaflet'), require('screenfull'));
} else {
// Assume 'leaflet' and 'screenfull' are loaded into global variable already
factory(root.L, root.screenfull);
}
}(typeof self !== 'undefined' ? self : this, function (leaflet, screenfull) {
'use strict';
leaflet.Control.FullScreen = leaflet.Control.extend({
options: {
position: 'topleft',
title: 'Full Screen',
titleCancel: 'Exit Full Screen',
forceSeparateButton: false,
forcePseudoFullscreen: false,
fullscreenElement: false
},
_screenfull: screenfull,
onAdd: function (map) {
var className = 'leaflet-control-zoom-fullscreen', container, content = '';
if (map.zoomControl && !this.options.forceSeparateButton) {
container = map.zoomControl._container;
} else {
container = leaflet.DomUtil.create('div', 'leaflet-bar');
}
if (this.options.content) {
content = this.options.content;
} else {
className += ' fullscreen-icon';
}
this._createButton(this.options.title, className, content, container, this.toggleFullScreen, this);
this._map.fullscreenControl = this;
this._map.on('enterFullscreen exitFullscreen', this._toggleState, this);
return container;
},
onRemove: function () {
leaflet.DomEvent
.off(this.link, 'click', leaflet.DomEvent.stop)
.off(this.link, 'click', this.toggleFullScreen, this);
leaflet.DomEvent
.off(this._container, this._screenfull.raw.fullscreenchange, leaflet.DomEvent.stop)
.off(this._container, this._screenfull.raw.fullscreenchange, this._handleFullscreenChange, this);
leaflet.DomEvent
.off(document, this._screenfull.raw.fullscreenchange, leaflet.DomEvent.stop)
.off(document, this._screenfull.raw.fullscreenchange, this._handleFullscreenChange, this);
},
_createButton: function (title, className, content, container, fn, context) {
this.link = leaflet.DomUtil.create('a', className, container);
this.link.href = '#';
this.link.title = title;
this.link.innerHTML = content;
this.link.setAttribute('role', 'button');
this.link.setAttribute('aria-label', title);
L.DomEvent.disableClickPropagation(container);
leaflet.DomEvent
.on(this.link, 'click', leaflet.DomEvent.stop)
.on(this.link, 'click', fn, context);
leaflet.DomEvent
.on(container, this._screenfull.raw.fullscreenchange, leaflet.DomEvent.stop)
.on(container, this._screenfull.raw.fullscreenchange, this._handleFullscreenChange, context);
leaflet.DomEvent
.on(document, this._screenfull.raw.fullscreenchange, leaflet.DomEvent.stop)
.on(document, this._screenfull.raw.fullscreenchange, this._handleFullscreenChange, context);
return this.link;
},
toggleFullScreen: function () {
var map = this._map;
map._exitFired = false;
if (map._isFullscreen) {
if (this._screenfull.isEnabled && !this.options.forcePseudoFullscreen) {
this._screenfull.exit();
} else {
leaflet.DomUtil.removeClass(this.options.fullscreenElement ? this.options.fullscreenElement : map._container, 'leaflet-pseudo-fullscreen');
map.invalidateSize();
}
map.fire('exitFullscreen');
map._exitFired = true;
map._isFullscreen = false;
}
else {
if (this._screenfull.isEnabled && !this.options.forcePseudoFullscreen) {
this._screenfull.request(this.options.fullscreenElement ? this.options.fullscreenElement : map._container);
} else {
leaflet.DomUtil.addClass(this.options.fullscreenElement ? this.options.fullscreenElement : map._container, 'leaflet-pseudo-fullscreen');
map.invalidateSize();
}
map.fire('enterFullscreen');
map._isFullscreen = true;
}
},
_toggleState: function () {
this.link.title = this._map._isFullscreen ? this.options.title : this.options.titleCancel;
this._map._isFullscreen ? L.DomUtil.removeClass(this.link, 'leaflet-fullscreen-on') : L.DomUtil.addClass(this.link, 'leaflet-fullscreen-on');
},
_handleFullscreenChange: function () {
var map = this._map;
map.invalidateSize();
if (!this._screenfull.isFullscreen && !map._exitFired) {
map.fire('exitFullscreen');
map._exitFired = true;
map._isFullscreen = false;
}
}
});
leaflet.Map.include({
toggleFullscreen: function () {
this.fullscreenControl.toggleFullScreen();
}
});
leaflet.Map.addInitHook(function () {
if (this.options.fullscreenControl) {
this.addControl(leaflet.control.fullscreen(this.options.fullscreenControlOptions));
}
});
leaflet.control.fullscreen = function (options) {
return new leaflet.Control.FullScreen(options);
};
// must return an object containing also screenfull to make screenfull
// available outside of this package, if used as an amd module,
// as webpack cannot handle amd define with moduleid
return {leaflet: leaflet, screenfull: screenfull};
}));

View File

@ -0,0 +1 @@
<svg viewBox="0 0 26 52" xmlns="http://www.w3.org/2000/svg"><path d="M20.6 36.7H16a.9.9 0 0 1-.8-.8v-4.5c0-.2.2-.4.4-.4h1.4c.3 0 .5.2.5.4v3h3c.2 0 .4.2.4.5v1.4c0 .2-.2.4-.4.4zm-9.9-.8v-4.5c0-.2-.2-.4-.4-.4H8.9c-.3 0-.5.2-.5.4v3h-3c-.2 0-.4.2-.4.5v1.4c0 .2.2.4.4.4H10c.4 0 .8-.4.8-.8zm0 10.7V42c0-.4-.4-.8-.8-.8H5.4c-.2 0-.4.2-.4.4v1.4c0 .3.2.5.4.5h3v3c0 .2.2.4.5.4h1.4c.2 0 .4-.2.4-.4zm6.9 0v-3h3c.2 0 .4-.2.4-.5v-1.4c0-.2-.2-.4-.4-.4H16c-.4 0-.8.4-.8.8v4.5c0 .2.2.4.4.4h1.4c.3 0 .5-.2.5-.4zM5 10.3V5.9c0-.5.4-.9.9-.9h4.4c.2 0 .4.2.4.4V7c0 .2-.2.4-.4.4h-3v3c0 .2-.2.4-.4.4H5.4a.4.4 0 0 1-.4-.4zm10.3-4.9V7c0 .2.2.4.4.4h3v3c0 .2.2.4.4.4h1.5c.2 0 .4-.2.4-.4V5.9c0-.5-.4-.9-.9-.9h-4.4c-.2 0-.4.2-.4.4zm5.3 9.9H19c-.2 0-.4.2-.4.4v3h-3c-.2 0-.4.2-.4.4v1.5c0 .2.2.4.4.4h4.4c.5 0 .9-.4.9-.9v-4.4c0-.2-.2-.4-.4-.4zm-9.9 5.3V19c0-.2-.2-.4-.4-.4h-3v-3c0-.2-.2-.4-.4-.4H5.4c-.2 0-.4.2-.4.4v4.4c0 .5.4.9.9.9h4.4c.2 0 .4-.2.4-.4z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 945 B

View File

@ -0,0 +1,60 @@
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
/* IE 6-8 fallback colors */
.leaflet-oldie .marker-cluster-small {
background-color: rgb(181, 226, 140);
}
.leaflet-oldie .marker-cluster-small div {
background-color: rgb(110, 204, 57);
}
.leaflet-oldie .marker-cluster-medium {
background-color: rgb(241, 211, 87);
}
.leaflet-oldie .marker-cluster-medium div {
background-color: rgb(240, 194, 12);
}
.leaflet-oldie .marker-cluster-large {
background-color: rgb(253, 156, 115);
}
.leaflet-oldie .marker-cluster-large div {
background-color: rgb(241, 128, 23);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}

View File

@ -0,0 +1,14 @@
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}
.leaflet-cluster-spider-leg {
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,657 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-control-attribution svg {
display: inline !important;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,640 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg,
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-tile {
will-change: opacity;
}
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline: 0;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a,
.leaflet-bar a:hover {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path {
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.7);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover {
text-decoration: underline;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font-size: 11px;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 19px;
line-height: 1.4;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
border: none;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #999;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-clickable {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
.text-toggle[aria-expanded=false] .text-expanded {
display: none;
}
.text-toggle[aria-expanded=true] .text-collapsed {
display: none;
}
.sidebar-device-type {
color: #6c757d;
}
.sidebar-device-role {
font-size: .9rem;
color: #6c757d;
}
.leaflet-sidebar > .close {
text-decoration: none
}
.sidebar-cpe-list li {
list-style-type: decimal;
}

View File

@ -0,0 +1,86 @@
{% extends 'base/layout.html' %}
{% load static %}
{% block title %}Device map{% endblock %}
{% block head %}
{# Leaflet #}
<link rel="stylesheet" href="{% static 'netbox_device_map/leaflet/leaflet.css' %}">
<script src="{% static 'netbox_device_map/leaflet/leaflet.js' %}"></script>
{# Leaflet plugins #}
<link rel="stylesheet" href="{% static 'netbox_device_map/leaflet.fullscreen/Control.FullScreen.css' %}">
<script src="{% static 'netbox_device_map/leaflet.fullscreen/Control.FullScreen.js' %}"></script>
<script src="{% static 'netbox_device_map/js/svg-icon.js' %}"></script>
<link rel="stylesheet" href="{% static 'netbox_device_map/leaflet-sidebar/L.Control.Sidebar.css' %}">
<script src="{% static 'netbox_device_map/leaflet-sidebar/L.Control.Sidebar.js' %}"></script>
<link rel="stylesheet" href="{% static 'netbox_device_map/leaflet.markercluster/MarkerCluster.css' %}">
<link rel="stylesheet" href="{% static 'netbox_device_map/leaflet.markercluster/MarkerCluster.Default.css' %}">
<script src="{% static 'netbox_device_map/leaflet.markercluster/leaflet.markercluster.js' %}"></script>
<link rel="stylesheet" href="{% static 'netbox_device_map/style.css' %}">
{% endblock %}
{% block content %}
<ul class="nav nav-tabs px-3">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="map-tab" data-bs-toggle="tab" data-bs-target="#map"
type="button" role="tab" aria-controls="map" aria-selected="true">
Geographical map
{% if map_data %}<span class="badge bg-secondary">{{ map_data.markers|length }}</span>{% endif %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="filters-form-tab" data-bs-toggle="tab" data-bs-target="#filters-form"
type="button" role="tab" aria-controls="filters-form" aria-selected="true">
Filters
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="map" role="tabpanel" aria-labelledby="map-tab">
{% if map_data %}
<div class="mb-3">
{% if non_geolocated_devices %}
<p>
<a class="btn btn-sm btn-outline-secondary text-toggle" data-bs-toggle="collapse"
href="#notShownDevices"
role="button" aria-expanded="false" aria-controls="collapseExample">
<span class="text-collapsed">Show</span>
<span class="text-expanded">Hide</span>
devices are not geolocated ({{ non_geolocated_devices|length }})
</a>
</p>
<div class="collapse" id="notShownDevices">
<div class="card card-body">
<ul>
{% for device in non_geolocated_devices %}
<li>
<a href="{{ device.get_absolute_url }}"
target="_blank">{{ device.name }}</a>
<span class="separator">·</span>
<span class="text-muted">{{ device.device_role.name }}</span>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
<div id="{{ map_data.map_id }}" style="height: 700px"></div>
<div id="map-sidebar">
<div class="h3 sidebar-device-name"></div>
<div class="h6 sidebar-device-type"></div>
<div class="h5 sidebar-device-role"></div>
<div class="sidebar-cpe-list"></div>
</div>
{{ map_data|json_script:"map-data" }}
<script src="{% static 'netbox_device_map/js/map.js' %}"></script>
{% else %}
Please specify filtering criteria for displaying the map
{% endif %}
</div>
<div class="tab-pane" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
{% include 'inc/filter_list.html' %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,7 @@
from django.urls import path
from . import views
urlpatterns = [
path('', views.MapView.as_view(), name='map'),
path('connected-cpe/<int:pk>', views.ConnectedCpeAjaxView.as_view(), name='connected-cpe')
]

View File

@ -0,0 +1,74 @@
import re
from dcim.models import Device, Interface
from django.http import JsonResponse
from django.shortcuts import render
from django.views.generic import View
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models import Q
from . import forms
from .geographical_map import configure_leaflet_map
from .helpers import get_device_location, get_connected_devices
from .settings import plugin_settings
INTEGER_REGEXP = re.compile(r'\d+')
class MapView(PermissionRequiredMixin, View):
permission_required = ('ipam.view_vlan', 'dcim.view_device', 'dcim.view_devicerole', 'dcim.view_cable')
template_name = 'netbox_device_map/main.html'
form = forms.DeviceMapFilterForm
def get(self, request):
"""Device map view"""
form = self.form(request.GET)
if form.is_valid():
interfaces = Interface.objects.all()
vlan = form.cleaned_data['vlan']
interfaces = interfaces.filter(Q(untagged_vlan=vlan) | Q(tagged_vlans=vlan))
devices = Device.objects.filter(interfaces__in=interfaces).distinct()
if device_roles := form.cleaned_data['device_roles']:
devices = devices.filter(device_role__in=device_roles)
geolocated_devices = {d: coords for d in devices if (coords := get_device_location(d))}
non_geolocated_devices = set(devices) - set(geolocated_devices.keys())
map_data = configure_leaflet_map("geomap", geolocated_devices, form.cleaned_data['calculate_connections'])
map_data['vlan'] = vlan.id
return render(request, self.template_name, context=dict(
filter_form=form, map_data=map_data, non_geolocated_devices=non_geolocated_devices
))
return render(
request, self.template_name,
context=dict(filter_form=self.form(initial=request.GET))
)
class ConnectedCpeAjaxView(PermissionRequiredMixin, View):
permission_required = ('dcim.view_device', 'dcim.view_cable')
form = forms.ConnectedCpeForm
def get(self, request, **kwargs):
"""List of CPE devices connected to the specified node device"""
try:
device = Device.objects.get(pk=kwargs.get('pk'))
except Device.DoesNotExist:
return JsonResponse({'status': False, 'error': 'Device not found'}, status=404)
form = self.form(request.GET)
if form.is_valid():
data = form.cleaned_data
connected_devices_qs = get_connected_devices(device, vlan=data['vlan'])\
.filter(device_role__name=plugin_settings['cpe_device_role']).order_by()
connected_devices = [dict(id=d.id, name=d.name, url=d.get_absolute_url(), comments=d.comments)
for d in connected_devices_qs]
# Sorting list of CPE devices by the sequence of integers contained in the comments
connected_devices.sort(key=lambda d: tuple(int(n) for n in INTEGER_REGEXP.findall(d['comments'])))
return JsonResponse(dict(status=True, cpe_devices=connected_devices,
device_type=f'{device.device_type.manufacturer.name} {device.device_type.model}'))
else:
return JsonResponse({'status': False, 'error': 'Form fields filled out incorrectly',
'form_errors': form.errors}, status=404)

18
setup.py Normal file
View File

@ -0,0 +1,18 @@
from setuptools import setup, find_packages
with open('README.md', encoding='utf-8') as f:
long_description = f.read()
setup(
name='netbox-plugin-device-map',
version='0.1.0',
description='A simple device map with filter criteria',
long_description=long_description,
long_description_content_type='text/markdown',
author='Victor Golovanenko',
author_email='drygdryg2014@yandex.com',
license='GPL-3.0',
packages=find_packages(),
include_package_data=True,
zip_safe=False
)