Replace zone preview with JS Mapbox widget and simplify zone UI
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, onMounted, onWillUnmount, onWillUpdateProps, useRef } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
const MAPBOX_JS = "https://api.mapbox.com/mapbox-gl-js/v3.5.1/mapbox-gl.js";
|
||||
const MAPBOX_CSS = "https://api.mapbox.com/mapbox-gl-js/v3.5.1/mapbox-gl.css";
|
||||
|
||||
let mapboxAssetsPromise;
|
||||
|
||||
function loadMapboxAssets() {
|
||||
if (mapboxAssetsPromise) {
|
||||
return mapboxAssetsPromise;
|
||||
}
|
||||
mapboxAssetsPromise = new Promise((resolve, reject) => {
|
||||
const finish = () => {
|
||||
if (window.mapboxgl) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("Mapbox GL failed to load."));
|
||||
}
|
||||
};
|
||||
if (!document.querySelector(`link[href="${MAPBOX_CSS}"]`)) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = MAPBOX_CSS;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
if (window.mapboxgl) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
const existing = document.querySelector(`script[src="${MAPBOX_JS}"]`);
|
||||
if (existing) {
|
||||
existing.addEventListener("load", finish, { once: true });
|
||||
existing.addEventListener("error", () => reject(new Error("Cannot load Mapbox JS.")), { once: true });
|
||||
return;
|
||||
}
|
||||
const script = document.createElement("script");
|
||||
script.src = MAPBOX_JS;
|
||||
script.async = true;
|
||||
script.onload = finish;
|
||||
script.onerror = () => reject(new Error("Cannot load Mapbox JS."));
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
return mapboxAssetsPromise;
|
||||
}
|
||||
|
||||
function parsePolygonGeometry(rawValue) {
|
||||
if (!rawValue) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(rawValue);
|
||||
if (parsed?.type === "Feature") {
|
||||
return parsed.geometry;
|
||||
}
|
||||
if (parsed?.type === "Polygon") {
|
||||
return parsed;
|
||||
}
|
||||
if (Array.isArray(parsed)) {
|
||||
return { type: "Polygon", coordinates: [parsed] };
|
||||
}
|
||||
throw new Error("Polygon GeoJSON is invalid.");
|
||||
}
|
||||
|
||||
function getBoundsForPolygon(geometry) {
|
||||
if (!geometry || geometry.type !== "Polygon" || !Array.isArray(geometry.coordinates) || !Array.isArray(geometry.coordinates[0])) {
|
||||
return null;
|
||||
}
|
||||
const ring = geometry.coordinates[0];
|
||||
if (!ring.length) {
|
||||
return null;
|
||||
}
|
||||
let minLng = ring[0][0];
|
||||
let maxLng = ring[0][0];
|
||||
let minLat = ring[0][1];
|
||||
let maxLat = ring[0][1];
|
||||
for (const point of ring) {
|
||||
const lng = Number(point[0]);
|
||||
const lat = Number(point[1]);
|
||||
if (Number.isNaN(lng) || Number.isNaN(lat)) {
|
||||
continue;
|
||||
}
|
||||
minLng = Math.min(minLng, lng);
|
||||
maxLng = Math.max(maxLng, lng);
|
||||
minLat = Math.min(minLat, lat);
|
||||
maxLat = Math.max(maxLat, lat);
|
||||
}
|
||||
return [[minLng, minLat], [maxLng, maxLat]];
|
||||
}
|
||||
|
||||
export class MapboxPolygonPreviewField extends Component {
|
||||
static template = "dsrpt_repair_config.MapboxPolygonPreviewField";
|
||||
static props = { ...standardFieldProps };
|
||||
static supportedTypes = ["text"];
|
||||
|
||||
setup() {
|
||||
this.mapRef = useRef("map");
|
||||
this.map = null;
|
||||
this.lastRenderKey = null;
|
||||
|
||||
onMounted(() => this.renderPreview(this.props));
|
||||
onWillUpdateProps((nextProps) => this.renderPreview(nextProps));
|
||||
onWillUnmount(() => this.destroyMap());
|
||||
}
|
||||
|
||||
destroyMap() {
|
||||
if (this.map) {
|
||||
this.map.remove();
|
||||
this.map = null;
|
||||
}
|
||||
}
|
||||
|
||||
showMessage(container, message) {
|
||||
this.destroyMap();
|
||||
container.innerHTML = `<div class="o_mapbox_polygon_placeholder">${message}</div>`;
|
||||
}
|
||||
|
||||
async renderPreview(props) {
|
||||
const container = this.mapRef.el;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const rawValue = props.record.data[props.name] || "";
|
||||
const token = props.record.data.mapbox_token || "";
|
||||
const renderKey = `${token}::${rawValue}`;
|
||||
if (renderKey === this.lastRenderKey) {
|
||||
return;
|
||||
}
|
||||
this.lastRenderKey = renderKey;
|
||||
|
||||
if (!token) {
|
||||
this.showMessage(container, "Mapbox token is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
let geometry;
|
||||
try {
|
||||
geometry = parsePolygonGeometry(rawValue);
|
||||
} catch {
|
||||
this.showMessage(container, "Invalid polygon JSON.");
|
||||
return;
|
||||
}
|
||||
if (!geometry) {
|
||||
this.showMessage(container, "Add polygon GeoJSON to render map.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await loadMapboxAssets();
|
||||
} catch {
|
||||
this.showMessage(container, "Cannot load Mapbox assets.");
|
||||
return;
|
||||
}
|
||||
|
||||
window.mapboxgl.accessToken = token;
|
||||
container.innerHTML = "";
|
||||
if (!this.map) {
|
||||
this.map = new window.mapboxgl.Map({
|
||||
container,
|
||||
style: "mapbox://styles/mapbox/streets-v12",
|
||||
center: [-84.39, 33.75],
|
||||
zoom: 8,
|
||||
});
|
||||
this.map.addControl(new window.mapboxgl.NavigationControl({ showCompass: false }), "top-right");
|
||||
this.map.on("load", () => this.drawPolygon(geometry));
|
||||
return;
|
||||
}
|
||||
if (this.map.loaded()) {
|
||||
this.drawPolygon(geometry);
|
||||
} else {
|
||||
this.map.once("load", () => this.drawPolygon(geometry));
|
||||
}
|
||||
}
|
||||
|
||||
drawPolygon(geometry) {
|
||||
const sourceId = "zonePolygonSource";
|
||||
const fillLayerId = "zonePolygonFill";
|
||||
const lineLayerId = "zonePolygonLine";
|
||||
const feature = {
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry,
|
||||
};
|
||||
|
||||
if (this.map.getLayer(fillLayerId)) {
|
||||
this.map.removeLayer(fillLayerId);
|
||||
}
|
||||
if (this.map.getLayer(lineLayerId)) {
|
||||
this.map.removeLayer(lineLayerId);
|
||||
}
|
||||
if (this.map.getSource(sourceId)) {
|
||||
this.map.removeSource(sourceId);
|
||||
}
|
||||
|
||||
this.map.addSource(sourceId, { type: "geojson", data: feature });
|
||||
this.map.addLayer({
|
||||
id: fillLayerId,
|
||||
type: "fill",
|
||||
source: sourceId,
|
||||
paint: {
|
||||
"fill-color": "#1D4ED8",
|
||||
"fill-opacity": 0.24,
|
||||
},
|
||||
});
|
||||
this.map.addLayer({
|
||||
id: lineLayerId,
|
||||
type: "line",
|
||||
source: sourceId,
|
||||
paint: {
|
||||
"line-color": "#1E40AF",
|
||||
"line-width": 3,
|
||||
},
|
||||
});
|
||||
|
||||
const bounds = getBoundsForPolygon(geometry);
|
||||
if (bounds) {
|
||||
this.map.fitBounds(bounds, { padding: 32, duration: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("mapbox_polygon_preview", {
|
||||
component: MapboxPolygonPreviewField,
|
||||
displayName: "Mapbox Polygon Preview",
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.o_mapbox_polygon_widget {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
min-height: 420px;
|
||||
border: 1px solid #d7dde5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.o_mapbox_polygon_placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
color: #5f6b7a;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="dsrpt_repair_config.MapboxPolygonPreviewField">
|
||||
<div t-ref="map" class="o_mapbox_polygon_widget"/>
|
||||
</t>
|
||||
</templates>
|
||||
Reference in New Issue
Block a user