248 lines
8.0 KiB
JavaScript
248 lines
8.0 KiB
JavaScript
/** @odoo-module **/
|
|
|
|
import { Component, onMounted, onWillUnmount, useRef, useState } from "@odoo/owl";
|
|
import { registry } from "@web/core/registry";
|
|
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
|
import { rpc } from "@web/core/network/rpc";
|
|
|
|
export class FusionClockLocationMap extends Component {
|
|
static template = "fusion_clock.LocationMap";
|
|
static props = { ...standardFieldProps };
|
|
|
|
setup() {
|
|
this.mapRef = useRef("mapContainer");
|
|
this.map = null;
|
|
this.marker = null;
|
|
this.circle = null;
|
|
this._suppress = false;
|
|
this._interval = null;
|
|
this._AdvancedMarkerElement = null;
|
|
|
|
this.state = useState({
|
|
loading: true,
|
|
error: "",
|
|
mapVisible: false,
|
|
});
|
|
|
|
onMounted(() => this._init());
|
|
onWillUnmount(() => this._cleanup());
|
|
}
|
|
|
|
get lat() { return this.props.record.data.latitude || 0; }
|
|
get lng() { return this.props.record.data.longitude || 0; }
|
|
get radius() { return this.props.record.data.radius || 100; }
|
|
get color() { return this.props.record.data.color || "#10B981"; }
|
|
get hasCoords() { return this.lat !== 0 || this.lng !== 0; }
|
|
|
|
async _init() {
|
|
const apiKey = await this._getApiKey();
|
|
if (!apiKey) {
|
|
this.state.loading = false;
|
|
this.state.error = "Google Maps API key not configured. Set it in Fusion Clock Settings.";
|
|
return;
|
|
}
|
|
try {
|
|
await this._loadScript(apiKey);
|
|
} catch {
|
|
this.state.loading = false;
|
|
this.state.error = "Failed to load Google Maps API.";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
|
|
this._AdvancedMarkerElement = AdvancedMarkerElement;
|
|
} catch {
|
|
this.state.loading = false;
|
|
this.state.error = "Failed to load marker library.";
|
|
return;
|
|
}
|
|
|
|
this.state.loading = false;
|
|
|
|
if (!this.hasCoords) {
|
|
this._startWatcher();
|
|
return;
|
|
}
|
|
|
|
this.state.mapVisible = true;
|
|
await new Promise((r) => requestAnimationFrame(r));
|
|
await new Promise((r) => requestAnimationFrame(r));
|
|
this._buildMap();
|
|
}
|
|
|
|
_buildMap() {
|
|
const el = this.mapRef.el;
|
|
if (!el || !window.google || !this._AdvancedMarkerElement) return;
|
|
|
|
const center = { lat: this.lat, lng: this.lng };
|
|
|
|
this.map = new google.maps.Map(el, {
|
|
center,
|
|
zoom: 17,
|
|
mapId: "DEMO_MAP_ID",
|
|
mapTypeControl: true,
|
|
mapTypeControlOptions: {
|
|
style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
|
|
position: google.maps.ControlPosition.TOP_RIGHT,
|
|
mapTypeIds: ["roadmap", "satellite", "hybrid"],
|
|
},
|
|
streetViewControl: false,
|
|
fullscreenControl: true,
|
|
zoomControl: true,
|
|
gestureHandling: "greedy",
|
|
});
|
|
|
|
this._placeMarker(center);
|
|
this._drawCircle(center);
|
|
|
|
if (!this.props.readonly) {
|
|
this.map.addListener("click", (e) => {
|
|
const pos = { lat: e.latLng.lat(), lng: e.latLng.lng() };
|
|
this._placeMarker(pos);
|
|
this._drawCircle(pos);
|
|
this._suppress = true;
|
|
this._saveCoords(pos.lat, pos.lng);
|
|
});
|
|
}
|
|
|
|
this._startWatcher();
|
|
}
|
|
|
|
_placeMarker(pos) {
|
|
if (this.marker) {
|
|
this.marker.position = pos;
|
|
return;
|
|
}
|
|
|
|
this.marker = new this._AdvancedMarkerElement({
|
|
map: this.map,
|
|
position: pos,
|
|
gmpDraggable: !this.props.readonly,
|
|
title: "Drag to fine-tune location",
|
|
});
|
|
|
|
if (!this.props.readonly) {
|
|
this.marker.addListener("dragend", () => {
|
|
const p = this.marker.position;
|
|
const newPos = { lat: p.lat, lng: p.lng };
|
|
this._drawCircle(newPos);
|
|
this._suppress = true;
|
|
this._saveCoords(newPos.lat, newPos.lng);
|
|
});
|
|
}
|
|
}
|
|
|
|
_drawCircle(center) {
|
|
if (this.circle) {
|
|
this.circle.setCenter(center);
|
|
this.circle.setRadius(this.radius);
|
|
this.circle.setOptions({ fillColor: this.color, strokeColor: this.color });
|
|
} else {
|
|
this.circle = new google.maps.Circle({
|
|
map: this.map,
|
|
center,
|
|
radius: this.radius,
|
|
fillColor: this.color,
|
|
fillOpacity: 0.15,
|
|
strokeColor: this.color,
|
|
strokeOpacity: 0.6,
|
|
strokeWeight: 2,
|
|
clickable: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
async _saveCoords(lat, lng) {
|
|
if (this.props.readonly) return;
|
|
await this.props.record.update({
|
|
latitude: Math.round(lat * 10000000) / 10000000,
|
|
longitude: Math.round(lng * 10000000) / 10000000,
|
|
});
|
|
}
|
|
|
|
_startWatcher() {
|
|
if (this._interval) return;
|
|
this._lastLat = this.lat;
|
|
this._lastLng = this.lng;
|
|
this._lastRadius = this.radius;
|
|
|
|
this._interval = setInterval(() => {
|
|
const lat = this.lat;
|
|
const lng = this.lng;
|
|
const r = this.radius;
|
|
|
|
const moved = Math.abs(this._lastLat - lat) > 0.0000001
|
|
|| Math.abs(this._lastLng - lng) > 0.0000001;
|
|
const resized = Math.abs(this._lastRadius - r) > 0.5;
|
|
|
|
if (moved && this.map) {
|
|
this._lastLat = lat;
|
|
this._lastLng = lng;
|
|
if (this._suppress) { this._suppress = false; return; }
|
|
const pos = { lat, lng };
|
|
this._placeMarker(pos);
|
|
this._drawCircle(pos);
|
|
this.map.panTo(pos);
|
|
}
|
|
|
|
if (resized && this.circle) {
|
|
this._lastRadius = r;
|
|
this.circle.setRadius(r);
|
|
}
|
|
|
|
if (!this.map && this.hasCoords && !this.state.error && this._AdvancedMarkerElement) {
|
|
this.state.mapVisible = true;
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
this._buildMap();
|
|
});
|
|
});
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
async _getApiKey() {
|
|
try {
|
|
return await rpc("/web/dataset/call_kw", {
|
|
model: "ir.config_parameter",
|
|
method: "get_param",
|
|
args: ["fusion_clock.google_maps_api_key", ""],
|
|
kwargs: {},
|
|
}) || "";
|
|
} catch { return ""; }
|
|
}
|
|
|
|
async _loadScript(apiKey) {
|
|
if (window.google && window.google.maps) return;
|
|
return new Promise((resolve, reject) => {
|
|
if (document.querySelector('script[src*="maps.googleapis.com"]')) {
|
|
const t = setInterval(() => {
|
|
if (window.google && window.google.maps) { clearInterval(t); resolve(); }
|
|
}, 100);
|
|
setTimeout(() => { clearInterval(t); resolve(); }, 5000);
|
|
return;
|
|
}
|
|
const s = document.createElement("script");
|
|
s.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=__fclkMapCb`;
|
|
s.async = true;
|
|
s.defer = true;
|
|
window.__fclkMapCb = () => { delete window.__fclkMapCb; resolve(); };
|
|
s.onerror = () => reject(new Error("script load failed"));
|
|
document.head.appendChild(s);
|
|
});
|
|
}
|
|
|
|
_cleanup() {
|
|
if (this._interval) clearInterval(this._interval);
|
|
if (this.marker) { this.marker.map = null; this.marker = null; }
|
|
if (this.circle) { this.circle.setMap(null); this.circle = null; }
|
|
this.map = null;
|
|
}
|
|
}
|
|
|
|
registry.category("fields").add("fclk_location_map", {
|
|
component: FusionClockLocationMap,
|
|
supportedTypes: ["char"],
|
|
});
|