mirror of
https://github.com/teslamate-org/teslamate.git
synced 2026-01-24 21:06:08 +08:00
* Enable zoom on live view of the car. * Switch to using drag/pinch-to-zoom/scroll-to-zoom * Remove data-zoom attrbiute as parameters are directly set in the JS. * Directly set parameters for zoom instead of relying on attribute from DOM. * Switch to use hover/tap to show/hide zoom control.
322 lines
7.4 KiB
JavaScript
322 lines
7.4 KiB
JavaScript
const LANG = navigator.languages
|
||
? navigator.languages[0]
|
||
: navigator.language || navigator.userLanguage;
|
||
|
||
function toLocalTime(dateStr, opts) {
|
||
const date = new Date(dateStr);
|
||
|
||
return date instanceof Date && !isNaN(date.valueOf())
|
||
? date.toLocaleTimeString(LANG, opts)
|
||
: "–";
|
||
}
|
||
|
||
function toLocalDate(dateStr, opts) {
|
||
const date = new Date(dateStr);
|
||
|
||
return date instanceof Date && !isNaN(date.valueOf())
|
||
? date.toLocaleDateString(LANG, opts)
|
||
: "–";
|
||
}
|
||
|
||
export const Dropdown = {
|
||
mounted() {
|
||
const $el = this.el;
|
||
|
||
$el.querySelector("button").addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
$el.classList.toggle("is-active");
|
||
});
|
||
|
||
document.addEventListener("click", () => {
|
||
$el.classList.remove("is-active");
|
||
});
|
||
},
|
||
};
|
||
|
||
export const LocalTime = {
|
||
mounted() {
|
||
this.el.innerText = toLocalTime(this.el.dataset.date);
|
||
},
|
||
|
||
updated() {
|
||
this.el.innerText = toLocalTime(this.el.dataset.date);
|
||
},
|
||
};
|
||
|
||
export const LocalTimeRange = {
|
||
exec() {
|
||
const date = toLocalDate(this.el.dataset.startDate, {
|
||
year: "numeric",
|
||
month: "short",
|
||
day: "numeric",
|
||
});
|
||
|
||
const time = [this.el.dataset.startDate, this.el.dataset.endDate]
|
||
.map((date) =>
|
||
toLocalTime(date, { hour: "2-digit", minute: "2-digit", hour12: false })
|
||
)
|
||
.join(" – ");
|
||
|
||
this.el.innerText = `${date}, ${time}`;
|
||
},
|
||
|
||
mounted() {
|
||
this.exec();
|
||
},
|
||
updated() {
|
||
this.exec();
|
||
},
|
||
};
|
||
|
||
export const ConfirmGeoFenceDeletion = {
|
||
mounted() {
|
||
const { id, msg } = this.el.dataset;
|
||
|
||
this.el.addEventListener("click", () => {
|
||
if (window.confirm(msg)) {
|
||
this.pushEvent("delete", { id });
|
||
}
|
||
});
|
||
},
|
||
};
|
||
|
||
import {
|
||
Map as M,
|
||
TileLayer,
|
||
LatLng,
|
||
Control,
|
||
Marker,
|
||
Icon,
|
||
Circle,
|
||
CircleMarker,
|
||
} from "leaflet";
|
||
|
||
import markerIcon from "leaflet/dist/images/marker-icon.png";
|
||
import markerShadow from "leaflet/dist/images/marker-shadow.png";
|
||
|
||
const icon = new Icon({
|
||
iconUrl: markerIcon,
|
||
shadowUrl: markerShadow,
|
||
iconAnchor: [12, 40],
|
||
popupAnchor: [0, -25],
|
||
});
|
||
|
||
const DirectionArrow = CircleMarker.extend({
|
||
initialize(latLng, heading, options) {
|
||
this._heading = heading;
|
||
CircleMarker.prototype.initialize.call(this, latLng, {
|
||
fillOpacity: 1,
|
||
radius: 5,
|
||
...options,
|
||
});
|
||
},
|
||
|
||
setHeading(heading) {
|
||
this._heading = heading;
|
||
this.redraw();
|
||
},
|
||
|
||
_updatePath() {
|
||
const { x, y } = this._point;
|
||
|
||
if (this._heading === "")
|
||
return CircleMarker.prototype._updatePath.call(this);
|
||
|
||
this.getElement().setAttributeNS(
|
||
null,
|
||
"transform",
|
||
`translate(${x},${y}) rotate(${this._heading})`
|
||
);
|
||
|
||
const path = this._empty() ? "" : `M0,${3} L-4,${5} L0,${-5} L4,${5} z}`;
|
||
|
||
this._renderer._setPath(this, path);
|
||
},
|
||
});
|
||
|
||
function createMap(opts) {
|
||
const map = new M(opts.elId != null ? `map_${opts.elId}` : "map", opts);
|
||
|
||
const osm = new TileLayer(
|
||
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||
{ maxZoom: 19 }
|
||
);
|
||
|
||
if (opts.enableHybridLayer) {
|
||
const hybrid = new TileLayer(
|
||
"http://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}",
|
||
{ maxZoom: 20, subdomains: ["mt0", "mt1", "mt2", "mt3"] }
|
||
);
|
||
|
||
new Control.Layers({ OSM: osm, Hybrid: hybrid }).addTo(map);
|
||
}
|
||
|
||
map.addLayer(osm);
|
||
|
||
return map;
|
||
}
|
||
|
||
export const SimpleMap = {
|
||
mounted() {
|
||
const $position = document.querySelector(`#position_${this.el.dataset.id}`);
|
||
|
||
const map = createMap({
|
||
elId: this.el.dataset.id,
|
||
zoomControl: !!this.el.dataset.zoom,
|
||
boxZoom: false,
|
||
doubleClickZoom: false,
|
||
keyboard: false,
|
||
scrollWheelZoom: false,
|
||
tap: false,
|
||
dragging: false,
|
||
touchZoom: false,
|
||
});
|
||
|
||
const isArrow = this.el.dataset.marker === "arrow";
|
||
const [lat, lng, heading] = $position.value.split(",");
|
||
|
||
const marker = isArrow
|
||
? new DirectionArrow([lat, lng], heading)
|
||
: new Marker([lat, lng], { icon });
|
||
|
||
map.setView([lat, lng], 17);
|
||
marker.addTo(map);
|
||
|
||
map.removeControl(map.zoomControl);
|
||
|
||
map.on('mouseover', function(e) { map.addControl( map.zoomControl ); });
|
||
map.on('mouseout', function(e) { map.removeControl( map.zoomControl ); });
|
||
|
||
if (isArrow) {
|
||
const setView = () => {
|
||
const [lat, lng, heading] = $position.value.split(",");
|
||
marker.setHeading(heading);
|
||
marker.setLatLng([lat, lng]);
|
||
map.setView([lat, lng], map.getZoom());
|
||
};
|
||
|
||
$position.addEventListener("change", setView);
|
||
}
|
||
},
|
||
};
|
||
|
||
export const TriggerChange = {
|
||
updated() {
|
||
this.el.dispatchEvent(new CustomEvent("change"));
|
||
},
|
||
};
|
||
|
||
import("leaflet-control-geocoder");
|
||
import("@geoman-io/leaflet-geoman-free");
|
||
|
||
export const Map = {
|
||
mounted() {
|
||
const geoFence = (name) =>
|
||
document.querySelector(`input[name='geo_fence[${name}]']`);
|
||
|
||
const $radius = geoFence("radius");
|
||
const $latitude = geoFence("latitude");
|
||
const $longitude = geoFence("longitude");
|
||
|
||
const location = new LatLng($latitude.value, $longitude.value);
|
||
|
||
const controlOpts = {
|
||
position: "topleft",
|
||
cutPolygon: false,
|
||
drawCircle: false,
|
||
drawCircleMarker: false,
|
||
drawMarker: false,
|
||
drawPolygon: false,
|
||
drawPolyline: false,
|
||
drawRectangle: false,
|
||
removalMode: false,
|
||
};
|
||
|
||
const editOpts = {
|
||
allowSelfIntersection: false,
|
||
preventMarkerRemoval: true,
|
||
};
|
||
|
||
const map = createMap({ enableHybridLayer: true });
|
||
map.setView(location, 17, { animate: false });
|
||
map.pm.setLang(LANG);
|
||
map.pm.addControls(controlOpts);
|
||
map.pm.enableGlobalEditMode(editOpts);
|
||
|
||
const circle = new Circle(location, { radius: $radius.value })
|
||
.addTo(map)
|
||
.on("pm:edit", (e) => {
|
||
const { lat, lng } = e.target.getLatLng();
|
||
const radius = Math.round(e.target.getRadius());
|
||
|
||
$radius.value = radius;
|
||
$latitude.value = lat;
|
||
$longitude.value = lng;
|
||
|
||
const mBox = map.getBounds();
|
||
const cBox = circle.getBounds();
|
||
const bounds = mBox.contains(cBox) ? mBox : cBox;
|
||
map.fitBounds(bounds);
|
||
});
|
||
|
||
new Control.geocoder({ defaultMarkGeocode: false })
|
||
.on("markgeocode", (e) => {
|
||
const { bbox, center } = e.geocode;
|
||
|
||
const poly = L.polygon([
|
||
bbox.getSouthEast(),
|
||
bbox.getNorthEast(),
|
||
bbox.getNorthWest(),
|
||
bbox.getSouthWest(),
|
||
]);
|
||
|
||
circle.setLatLng(center);
|
||
|
||
const lBox = poly.getBounds();
|
||
const cBox = circle.getBounds();
|
||
const bounds = cBox.contains(lBox) ? cBox : lBox;
|
||
|
||
map.fitBounds(bounds);
|
||
map.pm.enableGlobalEditMode();
|
||
|
||
const { lat, lng } = center;
|
||
$latitude.value = lat;
|
||
$longitude.value = lng;
|
||
})
|
||
.addTo(map);
|
||
|
||
map.fitBounds(circle.getBounds(), { animate: false });
|
||
},
|
||
};
|
||
|
||
export const Modal = {
|
||
_freeze() {
|
||
document.documentElement.classList.add("is-clipped");
|
||
},
|
||
|
||
_unfreeze() {
|
||
document.documentElement.classList.remove("is-clipped");
|
||
},
|
||
|
||
mounted() {
|
||
// assumption: 'is-active' is always added after the initial mount
|
||
},
|
||
|
||
updated() {
|
||
this.el.classList.contains("is-active") ? this._freeze() : this._unfreeze();
|
||
},
|
||
|
||
destroyed() {
|
||
this._unfreeze();
|
||
},
|
||
};
|
||
|
||
export const NumericInput = {
|
||
mounted() {
|
||
this.el.onkeypress = (evt) => {
|
||
const charCode = evt.which ? evt.which : evt.keyCode;
|
||
return !(charCode > 31 && (charCode < 48 || charCode > 57));
|
||
};
|
||
},
|
||
};
|