diff --git a/odoo/addons/dsrpt_repair_config/__manifest__.py b/odoo/addons/dsrpt_repair_config/__manifest__.py index c0a8f64..9502f24 100644 --- a/odoo/addons/dsrpt_repair_config/__manifest__.py +++ b/odoo/addons/dsrpt_repair_config/__manifest__.py @@ -17,6 +17,13 @@ "views/repair_fsm_zone_action_main.xml", "views/menu.xml", ], + "assets": { + "web.assets_backend": [ + "dsrpt_repair_config/static/src/xml/mapbox_polygon_preview_field.xml", + "dsrpt_repair_config/static/src/js/mapbox_polygon_preview_field.js", + "dsrpt_repair_config/static/src/scss/mapbox_polygon_preview_field.scss", + ], + }, "installable": True, "application": True, } diff --git a/odoo/addons/dsrpt_repair_config/data/repair_fsm_zone_data_atlanta.xml b/odoo/addons/dsrpt_repair_config/data/repair_fsm_zone_data_atlanta.xml index 10fe55f..1e0bd4b 100644 --- a/odoo/addons/dsrpt_repair_config/data/repair_fsm_zone_data_atlanta.xml +++ b/odoo/addons/dsrpt_repair_config/data/repair_fsm_zone_data_atlanta.xml @@ -5,6 +5,5 @@ - active diff --git a/odoo/addons/dsrpt_repair_config/models/fsm_zone.py b/odoo/addons/dsrpt_repair_config/models/fsm_zone.py index 6ed4a01..9871f77 100644 --- a/odoo/addons/dsrpt_repair_config/models/fsm_zone.py +++ b/odoo/addons/dsrpt_repair_config/models/fsm_zone.py @@ -1,5 +1,4 @@ import json -from urllib.parse import quote from odoo import api, fields, models from odoo.exceptions import ValidationError @@ -14,40 +13,25 @@ class RepairFsmZone(models.Model): name = fields.Char(required=True, tracking=True) polygon_geojson = fields.Text( string="Polygon (GeoJSON)", + required=True, tracking=True, help="GeoJSON Polygon geometry. Coordinates order: [longitude, latitude].", ) - polygon_map_preview = fields.Html( - string="Polygon Map Preview", - compute="_compute_polygon_map_preview", - sanitize=False, - ) state = fields.Selection( selection=[ - ("draft", "Draft"), ("active", "Active"), ("archived", "Archived"), ], - default="draft", + default="active", tracking=True, - group_expand="_group_expand_states", ) + mapbox_token = fields.Char(compute="_compute_mapbox_token") - def _group_expand_states(self, states, domain, order): - return [key for key, _label in self._fields["state"].selection] - - def action_set_active(self): + @api.depends_context("uid") + def _compute_mapbox_token(self): + token = self.env["ir.config_parameter"].sudo().get_param("dsrpt_repair_config.mapbox_token") or "" for rec in self: - if not rec.polygon_geojson: - raise ValidationError("Polygon is required before activating the FSM Zone.") - rec._extract_polygon_points() - self.write({"state": "active"}) - - def action_archive(self): - self.write({"state": "archived"}) - - def action_reset_draft(self): - self.write({"state": "draft"}) + rec.mapbox_token = token @staticmethod def _point_in_polygon(longitude, latitude, points): @@ -108,51 +92,7 @@ class RepairFsmZone(models.Model): points = self._extract_polygon_points() return self._point_in_polygon(float(longitude), float(latitude), points) - def action_open_polygon_in_map(self): - self.ensure_one() - encoded = quote(self.polygon_geojson or '{"type":"Polygon","coordinates":[[]]}') - return { - "type": "ir.actions.act_url", - "url": f"https://geojson.io/#data=data:application/json,{encoded}", - "target": "new", - } - - def action_open_point_picker_map(self): - return { - "type": "ir.actions.act_url", - "url": "https://www.openstreetmap.org", - "target": "new", - } - - @api.depends("polygon_geojson") - def _compute_polygon_map_preview(self): - token = self.env["ir.config_parameter"].sudo().get_param("dsrpt_repair_config.mapbox_token") - for rec in self: - if not rec.polygon_geojson: - rec.polygon_map_preview = "
No polygon yet.
" - continue - if not token: - rec.polygon_map_preview = "
Set Mapbox token in system parameter dsrpt_repair_config.mapbox_token.
" - continue - try: - data = json.loads(rec.polygon_geojson) - except Exception: - rec.polygon_map_preview = "
Invalid polygon JSON.
" - continue - feature = {"type": "Feature", "geometry": data, "properties": {"name": rec.name or "Zone"}} - overlay = quote(json.dumps(feature, separators=(",", ":"))) - token_encoded = quote(token) - static_url = ( - "https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/" - f"geojson({overlay})/auto/1100x420?padding=40&access_token={token_encoded}" - ) - rec.polygon_map_preview = ( - f'Zone polygon map preview' - ) - @api.constrains("polygon_geojson") def _check_polygon_geojson(self): for rec in self: - if rec.polygon_geojson: - rec._extract_polygon_points() + rec._extract_polygon_points() diff --git a/odoo/addons/dsrpt_repair_config/static/src/js/mapbox_polygon_preview_field.js b/odoo/addons/dsrpt_repair_config/static/src/js/mapbox_polygon_preview_field.js new file mode 100644 index 0000000..45a66c0 --- /dev/null +++ b/odoo/addons/dsrpt_repair_config/static/src/js/mapbox_polygon_preview_field.js @@ -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 = `
${message}
`; + } + + 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", +}); + diff --git a/odoo/addons/dsrpt_repair_config/static/src/scss/mapbox_polygon_preview_field.scss b/odoo/addons/dsrpt_repair_config/static/src/scss/mapbox_polygon_preview_field.scss new file mode 100644 index 0000000..c453ec2 --- /dev/null +++ b/odoo/addons/dsrpt_repair_config/static/src/scss/mapbox_polygon_preview_field.scss @@ -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; +} diff --git a/odoo/addons/dsrpt_repair_config/static/src/xml/mapbox_polygon_preview_field.xml b/odoo/addons/dsrpt_repair_config/static/src/xml/mapbox_polygon_preview_field.xml new file mode 100644 index 0000000..d959ee3 --- /dev/null +++ b/odoo/addons/dsrpt_repair_config/static/src/xml/mapbox_polygon_preview_field.xml @@ -0,0 +1,6 @@ + + + +
+ + diff --git a/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_form.xml b/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_form.xml index 451596d..1c118c9 100644 --- a/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_form.xml +++ b/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_form.xml @@ -5,14 +5,6 @@ repair.fsm.zone
-
-
@@ -21,10 +13,10 @@ - + + -
diff --git a/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_kanban.xml b/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_kanban.xml index a00d26e..d213ff8 100644 --- a/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_kanban.xml +++ b/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_kanban.xml @@ -4,9 +4,8 @@ repair.fsm.zone.kanban repair.fsm.zone - + -
diff --git a/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_list.xml b/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_list.xml index eacc2a0..b9b76d1 100644 --- a/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_list.xml +++ b/odoo/addons/dsrpt_repair_config/views/repair_fsm_zone_view_list.xml @@ -6,7 +6,6 @@ -