mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-19 20:12:08 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
|
|
@ -0,0 +1,402 @@
|
|||
/* OpenLayers Style */
|
||||
:root,
|
||||
:host {
|
||||
--ol-background-color: white;
|
||||
--ol-accent-background-color: #f5f5f5;
|
||||
--ol-subtle-background-color: rgba(128, 128, 128, 0.25);
|
||||
--ol-partial-background-color: rgba(255, 255, 255, 0.75);
|
||||
--ol-foreground-color: #333333;
|
||||
--ol-subtle-foreground-color: #666666;
|
||||
--ol-brand-color: #00aaff;
|
||||
}
|
||||
|
||||
.ol-box {
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
border: 1.5px solid var(--ol-background-color);
|
||||
background-color: var(--ol-partial-background-color);
|
||||
}
|
||||
|
||||
.ol-mouse-position {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ol-scale-line {
|
||||
background: var(--ol-partial-background-color);
|
||||
border-radius: 4px;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ol-scale-line-inner {
|
||||
border: 1px solid var(--ol-subtle-foreground-color);
|
||||
border-top: none;
|
||||
color: var(--ol-foreground-color);
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
margin: 1px;
|
||||
will-change: contents, width;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.ol-scale-bar {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.ol-scale-bar-inner {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ol-scale-step-marker {
|
||||
width: 1px;
|
||||
height: 15px;
|
||||
background-color: var(--ol-foreground-color);
|
||||
float: right;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.ol-scale-step-text {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
font-size: 10px;
|
||||
z-index: 11;
|
||||
color: var(--ol-foreground-color);
|
||||
text-shadow: -1.5px 0 var(--ol-partial-background-color),
|
||||
0 1.5px var(--ol-partial-background-color),
|
||||
1.5px 0 var(--ol-partial-background-color),
|
||||
0 -1.5px var(--ol-partial-background-color);
|
||||
}
|
||||
|
||||
.ol-scale-text {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
bottom: 25px;
|
||||
color: var(--ol-foreground-color);
|
||||
text-shadow: -1.5px 0 var(--ol-partial-background-color),
|
||||
0 1.5px var(--ol-partial-background-color),
|
||||
1.5px 0 var(--ol-partial-background-color),
|
||||
0 -1.5px var(--ol-partial-background-color);
|
||||
}
|
||||
|
||||
.ol-scale-singlebar {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
z-index: 9;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--ol-foreground-color);
|
||||
}
|
||||
|
||||
.ol-scale-singlebar-even {
|
||||
background-color: var(--ol-subtle-foreground-color);
|
||||
}
|
||||
|
||||
.ol-scale-singlebar-odd {
|
||||
background-color: var(--ol-background-color);
|
||||
}
|
||||
|
||||
.ol-unsupported {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ol-viewport,
|
||||
.ol-unselectable {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.ol-viewport canvas {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.ol-selectable {
|
||||
-webkit-touch-callout: default;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.ol-grabbing {
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.ol-grab {
|
||||
cursor: move;
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.ol-control {
|
||||
position: absolute;
|
||||
background-color: var(--ol-subtle-background-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ol-zoom {
|
||||
top: 0.5em;
|
||||
left: 0.5em;
|
||||
}
|
||||
|
||||
.draw-control {
|
||||
top: 4em;
|
||||
left: 0.5em;
|
||||
}
|
||||
|
||||
.select-control {
|
||||
top: 8em;
|
||||
left: 0.5em;
|
||||
}
|
||||
|
||||
.edit-control {
|
||||
top: 6em;
|
||||
left: 0.5em;
|
||||
}
|
||||
|
||||
#map-legend {
|
||||
bottom: 17em;
|
||||
right: 24em;
|
||||
z-index: 1054;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.legend {
|
||||
margin-bottom: 15px;
|
||||
display: none;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.selected-control > i {
|
||||
color: #71639e;
|
||||
}
|
||||
|
||||
.ol-rotate {
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
transition: opacity 0.25s linear, visibility 0s linear;
|
||||
}
|
||||
|
||||
.ol-rotate.ol-hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.25s linear, visibility 0s linear 0.25s;
|
||||
}
|
||||
|
||||
.ol-zoom-extent {
|
||||
top: 4.643em;
|
||||
left: 0.5em;
|
||||
}
|
||||
|
||||
.ol-full-screen {
|
||||
right: 0.5em;
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
.ol-control button {
|
||||
display: block;
|
||||
margin: 1px;
|
||||
padding: 0;
|
||||
color: var(--ol-subtle-foreground-color);
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
text-align: center;
|
||||
height: 1.375em;
|
||||
width: 1.375em;
|
||||
line-height: 0.4em;
|
||||
background-color: var(--ol-background-color);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.ol-control button::-moz-focus-inner {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ol-zoom-extent button {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.ol-compass {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.ol-touch .ol-control button {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.ol-touch .ol-zoom-extent {
|
||||
top: 5.5em;
|
||||
}
|
||||
|
||||
.ol-control button:hover,
|
||||
.ol-control button:focus {
|
||||
text-decoration: none;
|
||||
outline: 1px solid var(--ol-subtle-foreground-color);
|
||||
color: var(--ol-foreground-color);
|
||||
}
|
||||
|
||||
.ol-zoom .ol-zoom-in {
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
.ol-zoom .ol-zoom-out {
|
||||
border-radius: 0 0 2px 2px;
|
||||
}
|
||||
|
||||
.ol-attribution {
|
||||
text-align: right;
|
||||
bottom: 0.5em;
|
||||
right: 0.5em;
|
||||
max-width: calc(100% - 1.3em);
|
||||
display: flex;
|
||||
flex-flow: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ol-attribution a {
|
||||
color: var(--ol-subtle-foreground-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ol-attribution ul {
|
||||
margin: 0;
|
||||
padding: 1px 0.5em;
|
||||
color: var(--ol-foreground-color);
|
||||
text-shadow: 0 0 2px var(--ol-background-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ol-attribution li {
|
||||
display: inline;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.ol-attribution li:not(:last-child):after {
|
||||
content: " ";
|
||||
}
|
||||
|
||||
.ol-attribution img {
|
||||
max-height: 2em;
|
||||
max-width: inherit;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ol-attribution button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ol-attribution.ol-collapsed ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ol-attribution:not(.ol-collapsed) {
|
||||
background: var(--ol-partial-background-color);
|
||||
}
|
||||
|
||||
.ol-attribution.ol-uncollapsible {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
border-radius: 4px 0 0;
|
||||
}
|
||||
|
||||
.ol-attribution.ol-uncollapsible img {
|
||||
margin-top: -0.2em;
|
||||
max-height: 1.6em;
|
||||
}
|
||||
|
||||
.ol-attribution.ol-uncollapsible button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ol-zoomslider {
|
||||
top: 4.5em;
|
||||
left: 0.5em;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.ol-zoomslider button {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.ol-touch .ol-zoomslider {
|
||||
top: 5.5em;
|
||||
}
|
||||
|
||||
.ol-overviewmap {
|
||||
left: 0.5em;
|
||||
bottom: 0.5em;
|
||||
}
|
||||
|
||||
.ol-overviewmap.ol-uncollapsible {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: 0 4px 0 0;
|
||||
}
|
||||
|
||||
.ol-overviewmap .ol-overviewmap-map,
|
||||
.ol-overviewmap button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ol-overviewmap .ol-overviewmap-map {
|
||||
border: 1px solid var(--ol-subtle-foreground-color);
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.ol-overviewmap:not(.ol-collapsed) button {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ol-overviewmap.ol-collapsed .ol-overviewmap-map,
|
||||
.ol-overviewmap.ol-uncollapsible button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ol-overviewmap:not(.ol-collapsed) {
|
||||
background: var(--ol-subtle-background-color);
|
||||
}
|
||||
|
||||
.ol-overviewmap-box {
|
||||
border: 1.5px dotted var(--ol-subtle-foreground-color);
|
||||
}
|
||||
|
||||
.ol-overviewmap .ol-overviewmap-box:hover {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.ol-clear {
|
||||
top: 65px;
|
||||
left: 0.5em;
|
||||
}
|
||||
|
||||
.form-check {
|
||||
display: block;
|
||||
min-height: 1.625rem;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0.125rem;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
#popup-closer {
|
||||
cursor: pointer;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"globals": {
|
||||
"$": false,
|
||||
"_": false,
|
||||
"ol": false,
|
||||
"chroma": false,
|
||||
"geostats": false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import {reactive} from "@odoo/owl";
|
||||
|
||||
class RasterLayersStore {
|
||||
/**
|
||||
* Set raster layers to the store.
|
||||
* @param {*} rasters
|
||||
*/
|
||||
setRasters(rasters) {
|
||||
const newRasters = rasters.map((raster) => {
|
||||
Object.defineProperty(raster, "isVisible", {
|
||||
value: false,
|
||||
writable: true,
|
||||
});
|
||||
raster.isVisible = !raster.overlay;
|
||||
return raster;
|
||||
});
|
||||
this.rasters = newRasters;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when a raster layer is changed. This will notify observers of the change.
|
||||
* @param {*} newRastersLayer
|
||||
*/
|
||||
onRasterLayerChanged(newRastersLayer) {
|
||||
this.rasters = newRastersLayer;
|
||||
}
|
||||
|
||||
get rastersLayers() {
|
||||
return this.rasters;
|
||||
}
|
||||
|
||||
getRaster(id) {
|
||||
return this.rasters.find((el) => el.id === id);
|
||||
}
|
||||
|
||||
get count() {
|
||||
return this.rasters.length;
|
||||
}
|
||||
}
|
||||
export const rasterLayersStore = reactive(new RasterLayersStore());
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/** @odoo-module */
|
||||
import {reactive} from "@odoo/owl";
|
||||
|
||||
class VectorLayersStore {
|
||||
/**
|
||||
* Set vector layers to the store.
|
||||
* @param {*} rasters
|
||||
*/
|
||||
setVectors(vectors) {
|
||||
const newVectors = vectors.map((vector) => {
|
||||
Object.defineProperty(vector, "isVisible", {
|
||||
value: false,
|
||||
writable: true,
|
||||
});
|
||||
if (vector.active_on_startup) {
|
||||
vector.isVisible = true;
|
||||
}
|
||||
return vector;
|
||||
});
|
||||
this.vectors = newVectors;
|
||||
}
|
||||
|
||||
get vectorsLayers() {
|
||||
return this.vectors;
|
||||
}
|
||||
|
||||
getVector(resId) {
|
||||
return this.vectors.find((el) => el.resId === resId);
|
||||
}
|
||||
|
||||
get count() {
|
||||
return this.vectors.length;
|
||||
}
|
||||
}
|
||||
|
||||
export const vectorLayersStore = reactive(new VectorLayersStore());
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {addFieldDependencies} from "@web/views/utils";
|
||||
import {Field} from "@web/views/fields/field";
|
||||
import {Widget} from "@web/views/widgets/widget";
|
||||
import {XMLParser} from "@web/core/utils/xml";
|
||||
import {_lt} from "@web/core/l10n/translation";
|
||||
|
||||
export const INFO_BOX_ATTRIBUTE = "info_box";
|
||||
|
||||
export class GeoengineArchParser extends XMLParser {
|
||||
/**
|
||||
* Allow you to browse and process the xml template of the geoengine view.
|
||||
* @param {*} arch
|
||||
* @param {*} models
|
||||
* @param {*} modelName
|
||||
* @returns {Object}
|
||||
*/
|
||||
parse(arch, models, modelName) {
|
||||
const xmlDoc = this.parseXML(arch);
|
||||
const templateDocs = {};
|
||||
const fieldNodes = {};
|
||||
const jsClass = xmlDoc.getAttribute("js_class");
|
||||
const activeFields = {};
|
||||
const geoengineAttr = {};
|
||||
|
||||
this.visitXML(xmlDoc, (node) => {
|
||||
if (["geoengine"].includes(node.tagName)) {
|
||||
geoengineAttr.editable = Boolean(
|
||||
Number(xmlDoc.getAttribute("editable"))
|
||||
);
|
||||
}
|
||||
// Get the info box template
|
||||
if (node.hasAttribute("t-name")) {
|
||||
templateDocs[node.getAttribute("t-name")] = node;
|
||||
return;
|
||||
}
|
||||
if (node.tagName === "field") {
|
||||
const fieldInfo = Field.parseFieldNode(
|
||||
node,
|
||||
models,
|
||||
modelName,
|
||||
"geoengine",
|
||||
jsClass
|
||||
);
|
||||
const name = fieldInfo.name;
|
||||
fieldNodes[name] = fieldInfo;
|
||||
node.setAttribute("field_id", name);
|
||||
|
||||
addFieldDependencies(
|
||||
activeFields,
|
||||
models[modelName],
|
||||
fieldInfo.FieldComponent.fieldDependencies
|
||||
);
|
||||
}
|
||||
|
||||
if (node.tagName === "widget") {
|
||||
const {WidgetComponent} = Widget.parseWidgetNode(node);
|
||||
addFieldDependencies(
|
||||
activeFields,
|
||||
models[modelName],
|
||||
WidgetComponent.fieldDependencies
|
||||
);
|
||||
}
|
||||
});
|
||||
const infoBox = templateDocs[INFO_BOX_ATTRIBUTE];
|
||||
if (!infoBox) {
|
||||
throw new Error(_lt(`Missing ${INFO_BOX_ATTRIBUTE} template.`));
|
||||
}
|
||||
|
||||
for (const [key, field] of Object.entries(fieldNodes)) {
|
||||
activeFields[key] = field;
|
||||
}
|
||||
|
||||
return {
|
||||
arch,
|
||||
templateDocs,
|
||||
activeFields,
|
||||
fieldNodes,
|
||||
...geoengineAttr,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {ViewCompiler} from "@web/views/view_compiler";
|
||||
|
||||
export class GeoengineCompiler extends ViewCompiler {}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {Layout} from "@web/search/layout";
|
||||
import {useModel} from "@web/views/model";
|
||||
import {usePager} from "@web/search/pager_hook";
|
||||
import {useOwnedDialogs, useService} from "@web/core/utils/hooks";
|
||||
import {FormViewDialog} from "@web/views/view_dialogs/form_view_dialog";
|
||||
import {WarningDialog} from "@web/core/errors/error_dialogs";
|
||||
import {Component, useState} from "@odoo/owl";
|
||||
|
||||
export class GeoengineController extends Component {
|
||||
/**
|
||||
* Setup the controller by using the useModel hook.
|
||||
*/
|
||||
setup() {
|
||||
this.state = useState({isSavedOrDiscarded: false});
|
||||
this.actionService = useService("action");
|
||||
this.view = useService("view");
|
||||
this.addDialog = useOwnedDialogs();
|
||||
this.editable = this.props.archInfo.editable;
|
||||
this.model = useModel(this.props.Model, {
|
||||
activeFields: this.props.archInfo.activeFields,
|
||||
resModel: this.props.resModel,
|
||||
fields: this.props.fields,
|
||||
limit: this.props.limit,
|
||||
});
|
||||
|
||||
/**
|
||||
* Allow you to display records on the map thanks to the paging located
|
||||
* at the top right of the screen.
|
||||
*/
|
||||
usePager(() => {
|
||||
const list = this.model.root;
|
||||
const {count, limit, offset} = list;
|
||||
return {
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
total: count,
|
||||
onUpdate: async ({offset, limit}) => {
|
||||
await list.load({limit, offset});
|
||||
this.render(true);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Allow you to open the form editing view for the filled-in model.
|
||||
* @param {*} resModel
|
||||
* @param {*} resId
|
||||
*/
|
||||
async openRecord(resModel, resId) {
|
||||
const {views} = await this.view.loadViews({resModel, views: [[false, "form"]]});
|
||||
this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: resModel,
|
||||
views: [[views.form.id, "form"]],
|
||||
res_id: resId,
|
||||
target: "new",
|
||||
context: {edit: false, create: false},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When you finished drawing a new shape, this method is called to open form view and create the record.
|
||||
* @param {*} resModel
|
||||
* @param {*} field
|
||||
* @param {*} value
|
||||
*/
|
||||
async createRecord(resModel, field, value) {
|
||||
const {views} = await this.view.loadViews({resModel, views: [[false, "form"]]});
|
||||
const context = {};
|
||||
context[`default_${field}`] = value;
|
||||
|
||||
this.addDialog(FormViewDialog, {
|
||||
resModel: resModel,
|
||||
title: this.env._t("New record"),
|
||||
viewId: views.form.id,
|
||||
context,
|
||||
onRecordSaved: async () => await this.onSaveRecord(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when you have finished to create a new record.
|
||||
*/
|
||||
async onSaveRecord() {
|
||||
const offset = this.model.root.count + 1;
|
||||
await this.model.root.load({offset});
|
||||
this.render(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when you click on save button after edit a spatial representation.
|
||||
*/
|
||||
async onClickSave() {
|
||||
await this.model.root.editedRecord.save();
|
||||
this.state.isSavedOrDiscarded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when you click on discard button after edit a spatial representation.
|
||||
*/
|
||||
async onClickDiscard() {
|
||||
await this.model.root.editedRecord.discard();
|
||||
this.state.isSavedOrDiscarded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* When you have finished edtiting a spatial representation, this method is called to update the value.
|
||||
* @param {*} value
|
||||
*/
|
||||
async updateRecord(value) {
|
||||
this.state.isSavedOrDiscarded = false;
|
||||
const newValue = {};
|
||||
const key = Object.keys(this.model.root.fields).find(
|
||||
(el) => this.model.root.fields[el].geo_type !== undefined
|
||||
);
|
||||
newValue[key] = value;
|
||||
await this.model.root.editedRecord.update(newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method warns you if you start creating a record without having displayed the others.
|
||||
*/
|
||||
onDrawStart() {
|
||||
const {count, records} = this.model.root;
|
||||
if (records.length < count) {
|
||||
this.addDialog(WarningDialog, {
|
||||
title: this.env._t("Warning"),
|
||||
message: this.env._t(
|
||||
"You are about to create a new record without having displayed all the others. A risk of overlap could occur. Would you like to continue ?"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GeoengineController.template = "base_geoengine.GeoengineController";
|
||||
GeoengineController.components = {Layout};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
<t t-name="base_geoengine.GeoengineController" owl="1">
|
||||
<Layout display="props.display" className="'h-100'">
|
||||
<t t-set-slot="layout-buttons">
|
||||
<t t-if="model.root.editedRecord">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary o_list_button_save"
|
||||
data-hotkey="s"
|
||||
t-on-click.stop="onClickSave"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary o_list_button_discard"
|
||||
data-hotkey="j"
|
||||
t-on-click="onClickDiscard"
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
<t
|
||||
t-component="props.Renderer"
|
||||
isSavedOrDiscarded="state.isSavedOrDiscarded"
|
||||
archInfo="props.archInfo"
|
||||
data="model.root"
|
||||
editable="editable"
|
||||
openRecord.bind="openRecord"
|
||||
updateRecord.bind="updateRecord"
|
||||
onClickDiscard.bind="onClickDiscard"
|
||||
createRecord.bind="createRecord"
|
||||
onDrawStart.bind="onDrawStart"
|
||||
/>
|
||||
</Layout>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {Field} from "@web/views/fields/field";
|
||||
import {GeoengineCompiler} from "../geoengine_compiler.esm";
|
||||
import {INFO_BOX_ATTRIBUTE} from "../geoengine_arch_parser.esm";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {useViewCompiler} from "@web/views/view_compiler";
|
||||
import {Component, onWillUpdateProps} from "@odoo/owl";
|
||||
|
||||
const formatters = registry.category("formatters");
|
||||
|
||||
function getValue(record, fieldName) {
|
||||
const field = record.fields[fieldName];
|
||||
const value = record._values[fieldName];
|
||||
const formatter = formatters.get(field.type, String);
|
||||
return formatter(value, {field, data: record._values});
|
||||
}
|
||||
|
||||
export class GeoengineRecord extends Component {
|
||||
/**
|
||||
* Setup the record by compiling the arch and the info-box template.
|
||||
*/
|
||||
setup() {
|
||||
const {archInfo, templates} = this.props;
|
||||
const {arch} = archInfo;
|
||||
const ViewCompiler = this.constructor.Compiler;
|
||||
this.templates = useViewCompiler(ViewCompiler, arch, templates);
|
||||
this.createRecord(this.props);
|
||||
onWillUpdateProps(this.createRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create record with formatter.
|
||||
* @param {*} props
|
||||
*/
|
||||
createRecord(props) {
|
||||
const {record} = props;
|
||||
this.record = Object.create(null);
|
||||
for (const fieldName in record._values) {
|
||||
this.record[fieldName] = {
|
||||
get value() {
|
||||
return getValue(record, fieldName);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GeoengineRecord.template = "base_geoengine_GeoengineRecord";
|
||||
GeoengineRecord.Compiler = GeoengineCompiler;
|
||||
GeoengineRecord.components = {Field};
|
||||
GeoengineRecord.INFO_BOX_ATTRIBUTE = INFO_BOX_ATTRIBUTE;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="base_geoengine_GeoengineRecord" owl="1">
|
||||
<div>
|
||||
<t t-call="{{ templates[this.constructor.INFO_BOX_ATTRIBUTE] }}" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,67 @@
|
|||
#olmap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map_container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.view {
|
||||
max-height: 90%;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.menu {
|
||||
z-index: 2;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
padding-right: 0.5rem;
|
||||
padding-bottom: 3rem;
|
||||
padding-left: 2.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ol-popup {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #cccccc;
|
||||
bottom: 12px;
|
||||
left: -50px;
|
||||
min-width: 280px;
|
||||
}
|
||||
.ol-popup:after,
|
||||
.ol-popup:before {
|
||||
top: 100%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ol-popup:after {
|
||||
border-top-color: white;
|
||||
border-width: 10px;
|
||||
left: 48px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
.ol-popup:before {
|
||||
border-top-color: #cccccc;
|
||||
border-width: 11px;
|
||||
left: 48px;
|
||||
margin-left: -11px;
|
||||
}
|
||||
.ol-popup-closer {
|
||||
text-decoration: none;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 8px;
|
||||
}
|
||||
.ol-popup-closer:after {
|
||||
content: "✖";
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="base_geoengine.GeoengineRenderer" owl="1">
|
||||
<div class="d-flex view w-100">
|
||||
<LayersPanel model="props.data.resModel" vectorModel="vectorModel.root" />
|
||||
<div class="map_container">
|
||||
<div id="olmap" />
|
||||
</div>
|
||||
<RecordsPanel
|
||||
list="props.data"
|
||||
onDisplayPopupRecord.bind="onDisplayPopupRecord"
|
||||
zoomOnFeature.bind="zoomOnFeature"
|
||||
zoomOutOnFeature.bind="getOriginalZoom"
|
||||
/>
|
||||
</div>
|
||||
<div id="popup" class="ol-popup">
|
||||
<div
|
||||
id="popup-closer"
|
||||
class="ol-popup-closer text-primary"
|
||||
t-on-click="clickToHidePopup"
|
||||
/>
|
||||
<div id="popup-content" />
|
||||
<div class="row d-flex justify-content-center">
|
||||
<button
|
||||
t-on-click="onInfoBoxClicked"
|
||||
class="btn btn-secondary border w-50"
|
||||
>OPEN</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="map-legend" class="ol-control" />
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {_lt} from "@web/core/l10n/translation";
|
||||
import {GeoengineController} from "./geoengine_controller/geoengine_controller.esm";
|
||||
import {GeoengineRenderer} from "./geoengine_renderer/geoengine_renderer.esm";
|
||||
import {GeoengineArchParser} from "./geoengine_arch_parser.esm";
|
||||
import {GeoengineCompiler} from "./geoengine_compiler.esm";
|
||||
import {RelationalModel} from "@web/views/relational_model";
|
||||
import {registry} from "@web/core/registry";
|
||||
|
||||
export const geoengineView = {
|
||||
type: "geoengine",
|
||||
display_name: _lt("Geoengine"),
|
||||
icon: "fa fa-map-o",
|
||||
multiRecord: true,
|
||||
ArchParser: GeoengineArchParser,
|
||||
Controller: GeoengineController,
|
||||
Model: RelationalModel,
|
||||
Renderer: GeoengineRenderer,
|
||||
Compiler: GeoengineCompiler,
|
||||
|
||||
props: (genericProps, view) => {
|
||||
const {ArchParser} = view;
|
||||
const {arch, relatedModels, resModel} = genericProps;
|
||||
const archInfo = new ArchParser().parse(arch, relatedModels, resModel);
|
||||
|
||||
return {
|
||||
...genericProps,
|
||||
Model: view.Model,
|
||||
Renderer: view.Renderer,
|
||||
archInfo,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("views").add("geoengine", geoengineView);
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {CheckBox} from "@web/core/checkbox/checkbox";
|
||||
import {rasterLayersStore} from "../../../raster_layers_store.esm";
|
||||
import {vectorLayersStore} from "../../../vector_layers_store.esm";
|
||||
import {useOwnedDialogs, useService} from "@web/core/utils/hooks";
|
||||
import {DomainSelectorGeoFieldDialog} from "../../../widgets/domain_selector_geo_field/domain_selector_geo_field_dialog/domain_selector_geo_field_dialog.esm";
|
||||
import {FormViewDialog} from "@web/views/view_dialogs/form_view_dialog";
|
||||
import {useSortable} from "@web/core/utils/sortable";
|
||||
|
||||
import {Component, onWillStart, useRef, useState} from "@odoo/owl";
|
||||
|
||||
export class LayersPanel extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.actionService = useService("action");
|
||||
this.view = useService("view");
|
||||
this.rpc = useService("rpc");
|
||||
this.user = useService("user");
|
||||
this.state = useState({geoengineLayers: {}, isFolded: false});
|
||||
this.addDialog = useOwnedDialogs();
|
||||
let dataRowId = "";
|
||||
|
||||
/**
|
||||
* Call the model method "get_geoengine_layers" to get all the layers
|
||||
* in the database and add them to the store.
|
||||
*/
|
||||
onWillStart(async () => {
|
||||
await Promise.all([this.loadIsAdmin(), this.loadLayers()]);
|
||||
/**
|
||||
* Get resId of records to allow resequence of elements.
|
||||
*/
|
||||
this.state.geoengineLayers.actives.forEach((val) => {
|
||||
const element = this.props.vectorModel.records.find(
|
||||
(el) => el.resId === val.id
|
||||
);
|
||||
const obj = {id: element.id, resId: element.resId};
|
||||
Object.assign(val, obj);
|
||||
});
|
||||
// Set layers in the store
|
||||
rasterLayersStore.setRasters(this.state.geoengineLayers.backgrounds);
|
||||
vectorLayersStore.setVectors(this.state.geoengineLayers.actives);
|
||||
this.numberOfLayers = vectorLayersStore.count + rasterLayersStore.count;
|
||||
});
|
||||
|
||||
/**
|
||||
* Allows you to change the priority of the layer by sliding them over each other
|
||||
*/
|
||||
useSortable({
|
||||
ref: useRef("root"),
|
||||
elements: ".item",
|
||||
handle: ".fa-sort",
|
||||
onDragStart({element}) {
|
||||
dataRowId = element.dataset.id;
|
||||
},
|
||||
onDrop: (params) => this.sort(dataRowId, params),
|
||||
});
|
||||
}
|
||||
|
||||
async loadIsAdmin() {
|
||||
return this.user
|
||||
.hasGroup("base_geoengine.group_geoengine_admin")
|
||||
.then((result) => {
|
||||
this.isGeoengineAdmin = result;
|
||||
});
|
||||
}
|
||||
|
||||
async loadLayers() {
|
||||
return this.orm
|
||||
.call(this.props.model, "get_geoengine_layers", [])
|
||||
.then((result) => {
|
||||
this.state.geoengineLayers = result;
|
||||
});
|
||||
}
|
||||
|
||||
async sort(dataRowId, {previous}) {
|
||||
const refId = previous ? previous.dataset.id : null;
|
||||
this.resquence(dataRowId, refId);
|
||||
if (this.isGeoengineAdmin) {
|
||||
await this.resequenceAndUpdate(dataRowId, refId);
|
||||
} else {
|
||||
this.state.geoengineLayers.actives.forEach((element, index) => {
|
||||
this.onVectorChange(element, "onSequenceChanged", index + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resequence the order of layers but not update them (When a user modify them).
|
||||
* @param {*} dataRowId
|
||||
* @param {*} refId
|
||||
*/
|
||||
resquence(dataRowId, refId) {
|
||||
const fromIndex = this.state.geoengineLayers.actives.findIndex(
|
||||
(r) => r.id === dataRowId
|
||||
);
|
||||
let toIndex = 0;
|
||||
if (refId !== null) {
|
||||
const targetIndex = this.state.geoengineLayers.actives.findIndex(
|
||||
(r) => r.id === refId
|
||||
);
|
||||
toIndex = fromIndex > targetIndex ? targetIndex + 1 : targetIndex;
|
||||
}
|
||||
const [record] = this.state.geoengineLayers.actives.splice(fromIndex, 1);
|
||||
this.state.geoengineLayers.actives.splice(toIndex, 0, record);
|
||||
}
|
||||
/**
|
||||
* Resequence the order of layers and update them (When an admin modify them).
|
||||
* @param {*} dataRowId
|
||||
* @param {*} refId
|
||||
*/
|
||||
async resequenceAndUpdate(dataRowId, refId) {
|
||||
this.resequencePromise = this.props.vectorModel.resequence(dataRowId, refId, {
|
||||
handleField: "sequence",
|
||||
});
|
||||
await this.resequencePromise;
|
||||
this.props.vectorModel.records.forEach((element) => {
|
||||
this.onVectorChange(element, "onSequenceChanged", element.data.sequence);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when a raster layer is changed. The raster layer is set to visible and then
|
||||
* the method notifies the store of the change.
|
||||
* @param {*} layer
|
||||
*/
|
||||
onRasterChange(layer) {
|
||||
const indexRaster = rasterLayersStore.rastersLayers.findIndex(
|
||||
(raster) => raster.name === layer.name
|
||||
);
|
||||
const newRasters = rasterLayersStore.rastersLayers.map((item, index) => {
|
||||
if (index !== indexRaster) {
|
||||
item.isVisible = false;
|
||||
} else {
|
||||
item.isVisible = true;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
rasterLayersStore.onRasterLayerChanged(newRasters);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when a vector layer is changed. The vector layer is changed by an action and then
|
||||
* the method notifies the store of the change.
|
||||
* @param {*} layer
|
||||
* @param {*} action
|
||||
* @param {*} value
|
||||
*/
|
||||
async onVectorChange(layer, action, value) {
|
||||
vectorLayersStore.vectorsLayers.forEach((layer) => {
|
||||
layer.onDomainChanged = false;
|
||||
layer.onLayerChanged = false;
|
||||
layer.onSequenceChanged = false;
|
||||
});
|
||||
const vectorLayer = vectorLayersStore.getVector(layer.resId);
|
||||
switch (action) {
|
||||
case "onDomainChanged":
|
||||
Object.assign(vectorLayer, {
|
||||
model_domain: value,
|
||||
onDomainChanged: true,
|
||||
});
|
||||
break;
|
||||
case "onVisibleChanged":
|
||||
Object.assign(vectorLayer, {isVisible: value, onVisibleChanged: true});
|
||||
break;
|
||||
case "onLayerChanged":
|
||||
const geo_field_id = await this.orm.call(
|
||||
vectorLayer.resModel,
|
||||
"set_field_real_name",
|
||||
[value.geo_field_id]
|
||||
);
|
||||
const attribute_field_id = await this.orm.call(
|
||||
vectorLayer.resModel,
|
||||
"set_field_real_name",
|
||||
[value.attribute_field_id]
|
||||
);
|
||||
value.geo_field_id = geo_field_id;
|
||||
value.attribute_field_id = attribute_field_id;
|
||||
Object.assign(vectorLayer, {...value, onLayerChanged: true});
|
||||
break;
|
||||
case "onSequenceChanged":
|
||||
if (vectorLayer !== undefined) {
|
||||
Object.assign(vectorLayer, {
|
||||
sequence: value,
|
||||
onSequenceChanged: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onEditFilterButtonSelected(vector) {
|
||||
this.addDialog(DomainSelectorGeoFieldDialog, {
|
||||
resModel: vector.model,
|
||||
initialValue: vector.model_domain,
|
||||
readonly: false,
|
||||
isDebugMode: Boolean(this.env.debug),
|
||||
model: vector,
|
||||
onSelected: (value) => this.onEditFilterDomainChanged(vector, value),
|
||||
title: this.env._t("Domain editing"),
|
||||
});
|
||||
}
|
||||
|
||||
async onEditFilterDomainChanged(vector, value) {
|
||||
if (this.isGeoengineAdmin) {
|
||||
const record = this.props.vectorModel.records.find(
|
||||
(el) => el.resId === vector.resId
|
||||
);
|
||||
await record.update({model_domain: value});
|
||||
await record.save();
|
||||
}
|
||||
this.onVectorChange(vector, "onDomainChanged", value);
|
||||
}
|
||||
|
||||
async onEditButtonSelected(vector) {
|
||||
const view = await this.rpc("/web/action/load", {
|
||||
action_id: "base_geoengine.geo_vector_geoengine_view_action",
|
||||
});
|
||||
|
||||
this.addDialog(FormViewDialog, {
|
||||
resModel: vector.resModel,
|
||||
title: this.env._t("Editing vector layer"),
|
||||
viewId: view.view_id[0],
|
||||
resId: vector.resId,
|
||||
onRecordSaved: (record) =>
|
||||
this.onVectorChange(vector, "onLayerChanged", record.data),
|
||||
});
|
||||
}
|
||||
/**
|
||||
* This method allows you to open/close the panel.
|
||||
*/
|
||||
fold() {
|
||||
this.state.isFolded = !this.state.isFolded;
|
||||
}
|
||||
}
|
||||
|
||||
LayersPanel.template = "base_geoengine.LayersPanel";
|
||||
LayersPanel.props = {
|
||||
model: {type: String, optional: false},
|
||||
vectorModel: {type: Object, optional: false},
|
||||
};
|
||||
LayersPanel.components = {CheckBox};
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
.btn-edit {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.o_layer_panel {
|
||||
width: 250px;
|
||||
font-size: 1em;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.raster {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.fold {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.unfold {
|
||||
font-size: 1.3em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.o_layer_panel_fold {
|
||||
width: 50px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
background-color: #e9ecefd9;
|
||||
}
|
||||
|
||||
.title-panel {
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
font-size: 1.3em;
|
||||
margin-top: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
<t t-name="base_geoengine.LayersPanel" owl="1">
|
||||
<div
|
||||
class="menu border-end"
|
||||
t-attf-class="{{ state.isFolded ? 'o_layer_panel_fold ' : 'o_layer_panel bg-view' }}"
|
||||
>
|
||||
<div
|
||||
class="w-100 d-flex"
|
||||
t-attf-class="{{ state.isFolded ? 'justify-content-center' : 'justify-content-end'}}"
|
||||
>
|
||||
<button
|
||||
class="btn"
|
||||
t-attf-class="{{state.isFolded ? 'unfold text-900': 'fold'}}"
|
||||
t-on-click="fold"
|
||||
>
|
||||
<i class="fa fa-arrows-h" role="img" />
|
||||
</button>
|
||||
</div>
|
||||
<div t-if="!state.isFolded">
|
||||
<section class="o_search_panel_section">
|
||||
<header
|
||||
class="o_search_panel_section_header text-uppercase cursor-default"
|
||||
>
|
||||
<span class="fs-6 fw-bold">Vectors</span>
|
||||
</header>
|
||||
<div t-ref="root" class="root">
|
||||
<ul class="list p-0">
|
||||
<li
|
||||
t-foreach="state.geoengineLayers.actives"
|
||||
t-as="vector"
|
||||
t-key="vector.resId"
|
||||
class="item d-flex align-items-center"
|
||||
t-att-data-id="vector.id"
|
||||
>
|
||||
<i class="fa fa-sort m-3" />
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<CheckBox
|
||||
value="vector.isVisible"
|
||||
t-on-change="() => this.onVectorChange(vector, 'onVisibleChanged', !vector.isVisible)"
|
||||
>
|
||||
<t t-esc="vector.name" />
|
||||
</CheckBox>
|
||||
<button
|
||||
t-if="vector.model_id !== false"
|
||||
class="btn btn-edit"
|
||||
t-on-click.prevent="() => this.onEditFilterButtonSelected(vector)"
|
||||
>
|
||||
<i class="fa fa-filter text-primary" />
|
||||
</button>
|
||||
<button
|
||||
t-if="isGeoengineAdmin"
|
||||
class="btn btn-edit"
|
||||
t-on-click.prevent="() => this.onEditButtonSelected(vector)"
|
||||
>
|
||||
<i class="fa fa-edit text-primary" />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<section class="o_search_panel_section">
|
||||
<header
|
||||
class="o_search_panel_section_header pt-4 pb-2 text-uppercase cursor-default"
|
||||
>
|
||||
<span class="fs-6 fw-bold">Rasters</span>
|
||||
</header>
|
||||
<ul class="raster list-group d-block o_search_panel_field">
|
||||
<li
|
||||
t-foreach="state.geoengineLayers.backgrounds"
|
||||
t-as="layer"
|
||||
t-key="layer.id"
|
||||
>
|
||||
<div class="form-check o_radio_item" aria-atomic="true">
|
||||
<input
|
||||
type="radio"
|
||||
t-att-checked="layer.isVisible"
|
||||
class="form-check-input o_radio_input"
|
||||
t-att-id="layer.name"
|
||||
name="raster"
|
||||
t-att-value="layer.id"
|
||||
t-on-change="() => this.onRasterChange(layer)"
|
||||
/>
|
||||
<label
|
||||
class="form-check-label o_form_label"
|
||||
t-att-for="layer.name"
|
||||
t-esc="layer.name"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div t-else="" class="d-flex justify-content-center text-900">
|
||||
<span class="title-panel">Layers (<t t-esc="numberOfLayers" />)</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
import {SearchBarRecords} from "./search_bar_records/search_bar_records.esm";
|
||||
|
||||
import {
|
||||
Component,
|
||||
onWillRender,
|
||||
onWillStart,
|
||||
onWillUpdateProps,
|
||||
useState,
|
||||
} from "@odoo/owl";
|
||||
|
||||
export class RecordsPanel extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
isFolded: false,
|
||||
isClicked: 0,
|
||||
modelDescription: "",
|
||||
records: [],
|
||||
});
|
||||
this.orm = useService("orm");
|
||||
onWillStart(() => (this.state.records = this.props.list.records));
|
||||
onWillUpdateProps((nextProps) => (this.state.records = nextProps.list.records));
|
||||
onWillRender(async () => {
|
||||
// Retrieves the name of the current model
|
||||
const result = await this.orm.call("ir.model", "display_name_for", [
|
||||
[this.props.list.resModel],
|
||||
]);
|
||||
this.state.modelDescription = result[0].display_name;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows you to open/close the panel.
|
||||
*/
|
||||
fold() {
|
||||
this.state.isFolded = !this.state.isFolded;
|
||||
this.state.records = this.props.list.records;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method reacts to the click on a record.
|
||||
* @param {*} record
|
||||
*/
|
||||
onDisplayPopupRecord(record) {
|
||||
const rec = this.props.list.records.find(
|
||||
(val) => val._values.id === record.resId
|
||||
);
|
||||
this.state.isClicked = record.resId;
|
||||
this.props.onDisplayPopupRecord(rec);
|
||||
}
|
||||
|
||||
/**
|
||||
* When you press a key, it automatically performs the search.
|
||||
* @param {*} value
|
||||
*/
|
||||
onInputKeyup(value) {
|
||||
const val = this.filterItems(value, this.props.list.records);
|
||||
this.state.records = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows you to filter items according to the value passed in parameter.
|
||||
* @param {*} value
|
||||
* @param {*} items
|
||||
* @returns
|
||||
*/
|
||||
filterItems(value, items) {
|
||||
const lowerValue = value.toLowerCase();
|
||||
return items.filter((item) =>
|
||||
item.data && item.data.display_name
|
||||
? item.data.display_name.toLowerCase().indexOf(lowerValue) >= 0
|
||||
: false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RecordsPanel.template = "base_geoengine.RecordsPanel";
|
||||
RecordsPanel.components = {SearchBarRecords};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
.scroller {
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: auto;
|
||||
}
|
||||
|
||||
.record {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.record:hover {
|
||||
background-color: #f6f7fa;
|
||||
}
|
||||
|
||||
.btn-record {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
<t t-name="base_geoengine.RecordsPanel" owl="1">
|
||||
<div
|
||||
class="menu border-end scroller"
|
||||
t-attf-class="{{ state.isFolded ? 'o_layer_panel_fold ' : 'o_layer_panel bg-view' }}"
|
||||
>
|
||||
<div
|
||||
class="w-100 d-flex"
|
||||
t-attf-class="{{ state.isFolded ? 'justify-content-center' : 'justify-content-start'}}"
|
||||
>
|
||||
<button
|
||||
class="btn p-0 mb-3"
|
||||
t-attf-class="{{state.isFolded ? 'unfold text-900': 'fold'}}"
|
||||
t-on-click="fold"
|
||||
>
|
||||
<i class="fa fa-arrows-h" />
|
||||
</button>
|
||||
</div>
|
||||
<div t-if="!state.isFolded">
|
||||
<section class="o_search_panel_section">
|
||||
<header
|
||||
class="o_search_panel_section_header text-uppercase cursor-default mb-2"
|
||||
>
|
||||
<span class="fs-6 fw-bold">
|
||||
<t t-esc="state.modelDescription" />
|
||||
</span>
|
||||
</header>
|
||||
<SearchBarRecords onInputKeyup.bind="onInputKeyup" />
|
||||
<ul class="raster list-group d-block o_search_panel_field">
|
||||
<li
|
||||
class="mb-1 record d-flex justify-content-between"
|
||||
t-foreach="state.records"
|
||||
t-as="record"
|
||||
t-key="record.resId"
|
||||
>
|
||||
<button
|
||||
class="btn btn-record"
|
||||
t-on-click="() => this.onDisplayPopupRecord(record)"
|
||||
>
|
||||
<span>
|
||||
<t t-esc="record.data.display_name" />
|
||||
</span>
|
||||
</button>
|
||||
<t t-if="state.isClicked === record.resId">
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-search p-0"
|
||||
t-on-click="() => this.props.zoomOnFeature(record)"
|
||||
>
|
||||
<i
|
||||
class="fa fa-search-plus me-2 text-primary"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-search p-0"
|
||||
t-on-click="() => this.props.zoomOutOnFeature(record)"
|
||||
>
|
||||
<i
|
||||
class="fa fa-search-minus me-2 text-primary"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div t-else="" class="d-flex justify-content-center text-900">
|
||||
<span class="title-panel">Records (<t
|
||||
t-esc="props.list.records.length"
|
||||
/>)</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {Component, useRef} from "@odoo/owl";
|
||||
|
||||
export class SearchBarRecords extends Component {
|
||||
setup() {
|
||||
this.searchComponentRef = useRef("searchComponent");
|
||||
}
|
||||
|
||||
/**
|
||||
* When a key is pressed, the props onInputKeyup method is called.
|
||||
* @param {*} ev
|
||||
*/
|
||||
onInputKeyup(ev) {
|
||||
this.props.onInputKeyup(this.searchComponentRef.el.value);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
SearchBarRecords.template = "base_geoengine.SearchBarRecords";
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
<t t-name="base_geoengine.SearchBarRecords" owl="1">
|
||||
<div class="o_cp_searchview d-flex flex-grow-1" role="search">
|
||||
<div
|
||||
class="o_searchview pb-1 align-self-center border-bottom flex-grow-1"
|
||||
role="search"
|
||||
>
|
||||
<i
|
||||
class="o_searchview_icon oi oi-search"
|
||||
role="img"
|
||||
aria-label="Search..."
|
||||
title="Search..."
|
||||
/>
|
||||
<div class="o_searchview_input_container">
|
||||
<input
|
||||
t-ref="searchComponent"
|
||||
type="text"
|
||||
class="o_searchview_input"
|
||||
placeholder="Search..."
|
||||
t-on-keyup="onInputKeyup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {DomainField} from "@web/views/fields/domain/domain_field";
|
||||
import {registry} from "@web/core/registry";
|
||||
|
||||
export class DomainFieldExtend extends DomainField {
|
||||
async loadCount(props) {
|
||||
if (!this.getResModel(props)) {
|
||||
Object.assign(this.state, {recordCount: 0, isValid: true});
|
||||
}
|
||||
|
||||
let recordCount = 0;
|
||||
try {
|
||||
let value = props.value.slice();
|
||||
value = value.replace("not in active_ids", "not in");
|
||||
value = value.replace("in active_ids", "in");
|
||||
value = value.replace('"{ACTIVE_IDS}"', "[]");
|
||||
const domain = this.getDomain(value).toList(this.getContext(props));
|
||||
recordCount = await this.orm.silent.call(
|
||||
this.getResModel(props),
|
||||
"search_count",
|
||||
[domain],
|
||||
{context: this.getContext(props)}
|
||||
);
|
||||
} catch (_e) {
|
||||
// WOWL TODO: rethrow error when not the expected type
|
||||
Object.assign(this.state, {recordCount: 0, isValid: false});
|
||||
return;
|
||||
}
|
||||
Object.assign(this.state, {recordCount, isValid: true});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("domain", DomainFieldExtend, {force: true});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {Component, onRendered} from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* It allows you to set a default value for the field and a readonly property for the active_ids value.
|
||||
*/
|
||||
export class DomainSelectorFieldInputForActiveIds extends Component {
|
||||
setup() {
|
||||
onRendered(() => {
|
||||
if (this.props.value !== "{ACTIVE_IDS}") {
|
||||
this.props.update({value: "{ACTIVE_IDS}"});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
DomainSelectorFieldInputForActiveIds.template =
|
||||
"base_geoengine.DomainSelectorFieldInputForActiveIds";
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="base_geoengine.DomainSelectorFieldInputForActiveIds" owl="1">
|
||||
<input
|
||||
type="text"
|
||||
class="o_input o_domain_leaf_value_input"
|
||||
t-att-value="props.value"
|
||||
t-on-change="onChange"
|
||||
readonly="1"
|
||||
/>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {_lt} from "@web/core/l10n/translation";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {DomainSelectorGeoFieldInput} from "../domain_selector_geo_field_input/domain_selector_geo_field_input.esm";
|
||||
import {onDidChange} from "../domain_selector_operators.esm";
|
||||
import {Component} from "@odoo/owl";
|
||||
|
||||
const dsf = registry.category("domain_selector/fields");
|
||||
|
||||
/**
|
||||
* This class allows you to adapt the right-hand operand and the operator of the domain
|
||||
* if the selected field is of type geo_field.
|
||||
*/
|
||||
export class DomainSelectorGeoField extends Component {}
|
||||
Object.assign(DomainSelectorGeoField, {
|
||||
template: "base_geoengine.DomainSelectorGeoField",
|
||||
components: {
|
||||
DomainSelectorGeoFieldInput,
|
||||
},
|
||||
|
||||
onDidTypeChange() {
|
||||
return {value: ""};
|
||||
},
|
||||
|
||||
getOperators() {
|
||||
return [
|
||||
{
|
||||
category: "geospatial",
|
||||
label: _lt("geo_contains"),
|
||||
value: "geo_contains",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({operator}) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
{
|
||||
category: "geospatial",
|
||||
label: _lt("geo_greater"),
|
||||
value: "geo_greater",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({operator}) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
{
|
||||
category: "geospatial",
|
||||
label: _lt("geo_lesser"),
|
||||
value: "geo_lesser",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({operator}) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
{
|
||||
category: "geospatial",
|
||||
label: _lt("geo_equal"),
|
||||
value: "geo_equal",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({operator}) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
{
|
||||
category: "geospatial",
|
||||
label: _lt("geo_touch"),
|
||||
value: "geo_touch",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({operator}) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
{
|
||||
category: "geospatial",
|
||||
label: _lt("geo_within"),
|
||||
value: "geo_within",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({operator}) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
{
|
||||
category: "geospatial",
|
||||
label: _lt("geo_intersect"),
|
||||
value: "geo_intersect",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({operator}) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
dsf.add("geo_multi_polygon", DomainSelectorGeoField);
|
||||
dsf.add("geo_multi_point", DomainSelectorGeoField);
|
||||
dsf.add("geo_multi_line", DomainSelectorGeoField);
|
||||
dsf.add("geo_polygon", DomainSelectorGeoField);
|
||||
dsf.add("geo_point", DomainSelectorGeoField);
|
||||
dsf.add("geo_line", DomainSelectorGeoField);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="base_geoengine.DomainSelectorGeoField" owl="1">
|
||||
<div class="o_ds_value_cell">
|
||||
<DomainSelectorGeoFieldInput t-props="props" />
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {DomainSelectorDialog} from "@web/core/domain_selector_dialog/domain_selector_dialog";
|
||||
import {_t} from "@web/core/l10n/translation";
|
||||
|
||||
/**
|
||||
* This class is extended from DomainSelectorGeoField in order to be able to
|
||||
* modify the title of the dialog window and to add some props to it.
|
||||
*/
|
||||
export class DomainSelectorGeoFieldDialog extends DomainSelectorDialog {
|
||||
get dialogTitle() {
|
||||
return _t(this.props.title);
|
||||
}
|
||||
}
|
||||
|
||||
DomainSelectorGeoFieldDialog.template = "base_geoengine.DomainSelectorGeoFieldDialog";
|
||||
DomainSelectorGeoFieldDialog.props = {
|
||||
close: Function,
|
||||
className: {type: String, optional: true},
|
||||
resModel: String,
|
||||
readonly: {type: Boolean, optional: true},
|
||||
isDebugMode: {type: Boolean, optional: true},
|
||||
defaultLeafValue: {type: Array, optional: true},
|
||||
initialValue: {type: String, optional: true},
|
||||
onSelected: Function,
|
||||
fieldName: {type: String, optional: true},
|
||||
title: {type: String, optional: true},
|
||||
model: {type: Object, optional: true},
|
||||
};
|
||||
|
||||
DomainSelectorGeoFieldDialog.defaultProps = {
|
||||
initialValue: "",
|
||||
readonly: true,
|
||||
isDebugMode: false,
|
||||
title: "Domain",
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t
|
||||
t-name="base_geoengine.DomainSelectorGeoFieldDialog"
|
||||
t-inherit="web.DomainSelectorDialog"
|
||||
t-inherit-mode="primary"
|
||||
owl="1"
|
||||
>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {ModelFieldSelector} from "@web/core/model_field_selector/model_field_selector";
|
||||
import {ModelSelector} from "@web/core/model_selector/model_selector";
|
||||
import {Domain} from "@web/core/domain";
|
||||
import {evaluate} from "@web/core/py_js/py_interpreter";
|
||||
import {useOwnedDialogs} from "@web/core/utils/hooks";
|
||||
import {DomainSelectorGeoFieldDialog} from "../domain_selector_geo_field_dialog/domain_selector_geo_field_dialog.esm";
|
||||
|
||||
import {Component, onWillStart, onWillUpdateProps, useState} from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* This class correspond to the value of the right operand when a geo_field has
|
||||
* been selected.
|
||||
*/
|
||||
export class DomainSelectorGeoFieldInput extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
resModel: "",
|
||||
fieldName: "",
|
||||
subField: "",
|
||||
operator: "",
|
||||
value: "",
|
||||
domain: {},
|
||||
});
|
||||
this.addDialog = useOwnedDialogs();
|
||||
|
||||
/**
|
||||
* Before starting, if a value is already selected we had to know the fieldName and
|
||||
* the resModel.
|
||||
*/
|
||||
onWillStart(async () => {
|
||||
if (this.props.value instanceof Object) {
|
||||
this.defaultKey = Object.keys(this.props.value)[0];
|
||||
const index = this.defaultKey.lastIndexOf(".");
|
||||
this.state.fieldName = this.defaultKey.substring(index + 1);
|
||||
this.state.resModel = this.defaultKey.substring(0, index);
|
||||
this.loadDomain();
|
||||
} else {
|
||||
this.state.value = this.props.value;
|
||||
}
|
||||
});
|
||||
|
||||
onWillUpdateProps((nextProps) => this.loadDomain(nextProps));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allow the domain to be loaded into a state.
|
||||
* @param {*} nextProps
|
||||
*/
|
||||
loadDomain(nextProps) {
|
||||
const props = nextProps === undefined ? this.props : nextProps;
|
||||
if (this.defaultKey !== undefined) {
|
||||
this.key = this.defaultKey;
|
||||
}
|
||||
this.state.domain = new Domain(props.value[this.key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method updates the value of the right operand of the domain.
|
||||
* @param {*} value
|
||||
*/
|
||||
update(value) {
|
||||
this.key = this.state.resModel + "." + this.state.fieldName;
|
||||
const obj = {};
|
||||
let jsDomain = [];
|
||||
if (value !== undefined) {
|
||||
const domain = new Domain(value);
|
||||
jsDomain = evaluate(domain.ast, {});
|
||||
}
|
||||
obj[this.key] = jsDomain;
|
||||
this.props.update({value: obj});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method reacts to changes of the sub field name.
|
||||
* @param {*} fieldName
|
||||
*/
|
||||
async onFieldModelChange(fieldName) {
|
||||
this.state.fieldName = fieldName;
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* When we click on the edit button, this launches a dialog window allowing you to
|
||||
* edit the sub-domain.
|
||||
*/
|
||||
display() {
|
||||
const initialValue =
|
||||
this.state.domain !== undefined ? this.state.domain.toString() : "[]";
|
||||
this.addDialog(DomainSelectorGeoFieldDialog, {
|
||||
resModel: this.state.resModel,
|
||||
initialValue,
|
||||
readonly: false,
|
||||
isDebugMode: Boolean(this.env.debug),
|
||||
fieldName: this.state.fieldName,
|
||||
onSelected: (value) => this.update(value),
|
||||
title: this.env._t("Subdomain"),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method react to changes of the sub model.
|
||||
* @param {*} newModel
|
||||
*/
|
||||
onModelChange(newModel) {
|
||||
this.state.resModel = newModel.technical;
|
||||
this.state.fieldName = "id";
|
||||
this.state.subField = "";
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
DomainSelectorGeoFieldInput.template = "base_geoengine.DomainSelectorGeoFieldInput";
|
||||
DomainSelectorGeoFieldInput.components = {ModelFieldSelector, ModelSelector};
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="base_geoengine.DomainSelectorGeoFieldInput" owl="1">
|
||||
<div class="d-flex align-items-center">
|
||||
<ModelSelector
|
||||
value="state.resModel"
|
||||
onModelSelected.bind="onModelChange"
|
||||
/>
|
||||
<div t-if="state.resModel.length > 0" class="col-5">
|
||||
<ModelFieldSelector
|
||||
fieldName="state.fieldName"
|
||||
resModel="state.resModel"
|
||||
readonly="false"
|
||||
update="(name) => this.onFieldModelChange(name)"
|
||||
isDebugMode="false"
|
||||
/>
|
||||
</div>
|
||||
<button class="btn btn-link col-1" t-on-click.prevent="display">
|
||||
<i class="fa fa-pencil-square" />
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {registry} from "@web/core/registry";
|
||||
import {_lt} from "@web/core/l10n/translation";
|
||||
import {DomainSelectorFieldInput} from "@web/core/domain_selector/fields/domain_selector_field_input";
|
||||
import {DomainSelectorFieldInputForActiveIds} from "../domain_selector_field_input_for_active_ids/domain_selector_field_input_for_active_ids.esm";
|
||||
import {DomainSelectorFieldInputWithTags} from "@web/core/domain_selector/fields/domain_selector_field_input_with_tags";
|
||||
import {onDidChange} from "../domain_selector_operators.esm";
|
||||
|
||||
const dso = registry.category("domain_selector/operator");
|
||||
|
||||
import {Component} from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* This method is extended from DomainSelectorNumberField to add some operators
|
||||
* ("in active_ids", "not in active_ids", "in", "not in").
|
||||
*/
|
||||
export class DomainSelectorNumberFieldExtend extends Component {}
|
||||
Object.assign(DomainSelectorNumberFieldExtend, {
|
||||
template: "base_geoengine.DomainSelectorNumberFieldExtend",
|
||||
components: {
|
||||
DomainSelectorFieldInput,
|
||||
DomainSelectorFieldInputWithTags,
|
||||
DomainSelectorFieldInputForActiveIds,
|
||||
},
|
||||
|
||||
onDidTypeChange() {
|
||||
return {value: 0};
|
||||
},
|
||||
getOperators() {
|
||||
const addOperators = [
|
||||
{
|
||||
category: "active_ids",
|
||||
label: _lt("in active_ids"),
|
||||
value: "in active_ids",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({operator}) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
{
|
||||
category: "active_ids",
|
||||
label: _lt("not in active_ids"),
|
||||
value: "not in active_ids",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({operator}) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
];
|
||||
const operators = [
|
||||
"=",
|
||||
"!=",
|
||||
">",
|
||||
"<",
|
||||
">=",
|
||||
"<=",
|
||||
"ilike",
|
||||
"not ilike",
|
||||
"in",
|
||||
"not in",
|
||||
"set",
|
||||
"not set",
|
||||
].map((key) => dso.get(key));
|
||||
return operators.concat(addOperators);
|
||||
},
|
||||
});
|
||||
registry
|
||||
.category("domain_selector/fields")
|
||||
.add("integer", DomainSelectorNumberFieldExtend, {force: true});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="base_geoengine.DomainSelectorNumberFieldExtend" owl="1">
|
||||
<t t-if="props.operator.category === 'in'">
|
||||
<DomainSelectorFieldInputWithTags t-props="props" />
|
||||
</t>
|
||||
<t t-elif="props.operator.category === 'active_ids'">
|
||||
<DomainSelectorFieldInputForActiveIds
|
||||
value="props.value"
|
||||
update="props.update"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_ds_value_cell">
|
||||
<DomainSelectorFieldInput t-props="props" />
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* This is method is called when an operator changes its value.
|
||||
*/
|
||||
export function onDidChange(action) {
|
||||
return function (oldOperator, fieldChange) {
|
||||
if (this.category !== oldOperator.category) {
|
||||
return action(fieldChange);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.o_field_geo_multi_polygon,
|
||||
.o_field_geo_point,
|
||||
.o_field_geo_polygon,
|
||||
.o_field_geo_multi_line,
|
||||
.o_field_geo_multi_point,
|
||||
.o_field_geo_line {
|
||||
width: 100%;
|
||||
height: 550px;
|
||||
}
|
||||
.ol_map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
/**
|
||||
* Copyright 2023 ACSONE SA/NV
|
||||
*/
|
||||
|
||||
import {loadBundle} from "@web/core/assets";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
import {standardFieldProps} from "@web/views/fields/standard_field_props";
|
||||
|
||||
import {Component, onMounted, onRendered, onWillStart, useEffect} from "@odoo/owl";
|
||||
|
||||
export class FieldGeoEngineEditMap extends Component {
|
||||
setup() {
|
||||
// Allows you to have a unique id if you put the same field in the view several times
|
||||
this.id = `map_${Date.now()}`;
|
||||
this.orm = useService("orm");
|
||||
|
||||
onWillStart(() =>
|
||||
Promise.all([
|
||||
loadBundle({
|
||||
jsLibs: [
|
||||
"/base_geoengine/static/lib/ol-7.2.2/ol.js",
|
||||
"/base_geoengine/static/lib/chromajs-2.4.2/chroma.js",
|
||||
],
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
// Is executed when component is mounted.
|
||||
onMounted(async () => {
|
||||
const result = await this.orm.call(
|
||||
this.props.record.resModel,
|
||||
"get_edit_info_for_geo_column",
|
||||
[this.props.name]
|
||||
);
|
||||
this.projection = result.projection;
|
||||
this.defaultExtent = result.default_extent;
|
||||
this.defaultZoom = result.default_zoom;
|
||||
this.restrictedExtent = result.restricted_extent;
|
||||
this.srid = result.srid;
|
||||
this.createLayers();
|
||||
this.renderMap();
|
||||
this.setValue(this.props.value);
|
||||
});
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!this.props.readonly && this.map !== undefined) {
|
||||
this.setupControls();
|
||||
}
|
||||
},
|
||||
() => [this.props.value]
|
||||
);
|
||||
|
||||
// Is executed after component is rendered. When we use pagination.
|
||||
onRendered(() => {
|
||||
this.setValue(this.props.value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays geo data on the map using the collection of features.
|
||||
*/
|
||||
createVectorLayer() {
|
||||
this.features = new ol.Collection();
|
||||
this.source = new ol.source.Vector({features: this.features});
|
||||
const colorHex = this.props.color !== undefined ? this.props.color : "#ee9900";
|
||||
const opacity = this.props.opacity !== undefined ? this.props.opacity : 1;
|
||||
const color = chroma(colorHex).alpha(opacity).css();
|
||||
const fill = new ol.style.Fill({
|
||||
color: color,
|
||||
});
|
||||
const stroke = new ol.style.Stroke({
|
||||
color,
|
||||
width: 2,
|
||||
});
|
||||
return new ol.layer.Vector({
|
||||
source: this.source,
|
||||
style: new ol.style.Style({
|
||||
fill,
|
||||
stroke,
|
||||
image: new ol.style.Circle({
|
||||
radius: 5,
|
||||
fill,
|
||||
stroke,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the method that creates the layer to display the geo data on the map.
|
||||
*/
|
||||
createLayers() {
|
||||
this.vectorLayer = this.createVectorLayer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to centre the area defined for the user.
|
||||
* If there is an item to display.
|
||||
*/
|
||||
updateMapZoom() {
|
||||
if (this.source) {
|
||||
var extent = this.source.getExtent();
|
||||
var infinite_extent = [Infinity, Infinity, -Infinity, -Infinity];
|
||||
if (extent !== infinite_extent) {
|
||||
var map_view = this.map.getView();
|
||||
if (map_view) {
|
||||
map_view.fit(extent, {maxZoom: 14});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows you to centre the area defined for the user.
|
||||
* If there is not item to display.
|
||||
*/
|
||||
updateMapEmpty() {
|
||||
var map_view = this.map.getView();
|
||||
if (map_view) {
|
||||
var extent = this.defaultExtent.replace(/\s/g, "").split(",");
|
||||
extent = extent.map((coord) => Number(coord));
|
||||
map_view.fit(extent, {maxZoom: this.defaultZoom || 5});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the value passed in props, adds a new feature to the collection.
|
||||
* @param {*} value
|
||||
*/
|
||||
setValue(value) {
|
||||
if (this.map) {
|
||||
/**
|
||||
* If the value to be displayed is equal to the one passed in props, do nothing
|
||||
* otherwise clear the map and display the new value.
|
||||
*/
|
||||
if (this.displayValue == value) return;
|
||||
this.displayValue = value;
|
||||
var ft = new ol.Feature({
|
||||
geometry: new ol.format.GeoJSON().readGeometry(value),
|
||||
labelPoint: new ol.format.GeoJSON().readGeometry(value),
|
||||
});
|
||||
this.source.clear();
|
||||
this.source.addFeature(ft);
|
||||
|
||||
if (value) {
|
||||
this.updateMapZoom();
|
||||
} else {
|
||||
this.updateMapEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is triggered when the view changed. When we have finished drawing our geo data, or
|
||||
* when we clear the map.
|
||||
* @param {*} geometry
|
||||
*/
|
||||
onUIChange(geometry) {
|
||||
var value = null;
|
||||
if (geometry) {
|
||||
value = this.format.writeGeometry(geometry);
|
||||
}
|
||||
this.props.update(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow you to setup the trash button and the draw interaction.
|
||||
*/
|
||||
setupControls() {
|
||||
if (!this.props.value) {
|
||||
void (
|
||||
this.selectInteraction !== undefined &&
|
||||
this.map.removeInteraction(this.selectInteraction)
|
||||
);
|
||||
void (
|
||||
this.modifyInteraction !== undefined &&
|
||||
this.map.removeInteraction(this.modifyInteraction)
|
||||
);
|
||||
this.drawInteraction = new ol.interaction.Draw({
|
||||
type: this.geoType,
|
||||
source: this.source,
|
||||
});
|
||||
this.map.addInteraction(this.drawInteraction);
|
||||
|
||||
this.drawInteraction.on("drawend", (e) => {
|
||||
this.onUIChange(e.feature.getGeometry());
|
||||
});
|
||||
} else {
|
||||
void (
|
||||
this.drawInteraction !== undefined &&
|
||||
this.map.removeInteraction(this.drawInteraction)
|
||||
);
|
||||
this.selectInteraction = new ol.interaction.Select();
|
||||
this.modifyInteraction = new ol.interaction.Modify({
|
||||
features: this.selectInteraction.getFeatures(),
|
||||
});
|
||||
this.map.addInteraction(this.selectInteraction);
|
||||
this.map.addInteraction(this.modifyInteraction);
|
||||
|
||||
this.modifyInteraction.on("modifyend", (e) => {
|
||||
e.features.getArray().forEach((item) => {
|
||||
this.onUIChange(item.getGeometry());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const element = this.createTrashControl();
|
||||
|
||||
this.clearmapControl = new ol.control.Control({element: element});
|
||||
|
||||
this.map.addControl(this.clearmapControl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the trash button that clears the map.
|
||||
* @returns the div in which the button is located.
|
||||
*/
|
||||
createTrashControl() {
|
||||
const button = document.createElement("button");
|
||||
button.innerHTML = '<i class="fa fa-trash"/>';
|
||||
button.addEventListener("click", () => {
|
||||
this.source.clear();
|
||||
this.onUIChange(null);
|
||||
});
|
||||
const element = document.createElement("div");
|
||||
element.className = "ol-clear ol-unselectable ol-control";
|
||||
element.appendChild(button);
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the map in the div provided.
|
||||
*/
|
||||
renderMap() {
|
||||
this.map = new ol.Map({
|
||||
target: this.id,
|
||||
layers: [
|
||||
new ol.layer.Tile({
|
||||
source: new ol.source.OSM(),
|
||||
}),
|
||||
],
|
||||
view: new ol.View({
|
||||
center: [0, 0],
|
||||
zoom: 5,
|
||||
}),
|
||||
});
|
||||
this.map.addLayer(this.vectorLayer);
|
||||
this.format = new ol.format.GeoJSON({
|
||||
internalProjection: this.map.getView().getProjection(),
|
||||
externalProjection: "EPSG:" + this.srid,
|
||||
});
|
||||
|
||||
if (!this.props.readonly) {
|
||||
this.setupControls();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FieldGeoEngineEditMap.template = "base_geoengine.FieldGeoEngineEditMap";
|
||||
FieldGeoEngineEditMap.props = {
|
||||
...standardFieldProps,
|
||||
opacity: {type: Number, optional: true},
|
||||
color: {type: String, optional: true},
|
||||
};
|
||||
|
||||
FieldGeoEngineEditMap.extractProps = ({attrs}) => {
|
||||
return {
|
||||
opacity: attrs.options.opacity,
|
||||
color: attrs.options.color,
|
||||
};
|
||||
};
|
||||
|
||||
export class FieldGeoEngineEditMapMultiPolygon extends FieldGeoEngineEditMap {
|
||||
setup() {
|
||||
this.geoType = "MultiPolygon";
|
||||
super.setup();
|
||||
}
|
||||
}
|
||||
|
||||
export class FieldGeoEngineEditMapPolygon extends FieldGeoEngineEditMap {
|
||||
setup() {
|
||||
this.geoType = "Polygon";
|
||||
super.setup();
|
||||
}
|
||||
}
|
||||
|
||||
export class FieldGeoEngineEditMapPoint extends FieldGeoEngineEditMap {
|
||||
setup() {
|
||||
this.geoType = "Point";
|
||||
super.setup();
|
||||
}
|
||||
}
|
||||
|
||||
export class FieldGeoEngineEditMapMultiPoint extends FieldGeoEngineEditMap {
|
||||
setup() {
|
||||
this.geoType = "MultiPoint";
|
||||
super.setup();
|
||||
}
|
||||
}
|
||||
|
||||
export class FieldGeoEngineEditMapLine extends FieldGeoEngineEditMap {
|
||||
setup() {
|
||||
this.geoType = "LineString";
|
||||
super.setup();
|
||||
}
|
||||
}
|
||||
|
||||
export class FieldGeoEngineEditMapMultiLine extends FieldGeoEngineEditMap {
|
||||
setup() {
|
||||
this.geoType = "MultiLineString";
|
||||
super.setup();
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("geo_multi_polygon", FieldGeoEngineEditMapMultiPolygon);
|
||||
registry.category("fields").add("geo_polygon", FieldGeoEngineEditMapPolygon);
|
||||
registry.category("fields").add("geo_point", FieldGeoEngineEditMapPoint);
|
||||
registry.category("fields").add("geo_multi_point", FieldGeoEngineEditMapMultiPoint);
|
||||
registry.category("fields").add("geo_line", FieldGeoEngineEditMapLine);
|
||||
registry.category("fields").add("geo_multi_line", FieldGeoEngineEditMapMultiLine);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="base_geoengine.FieldGeoEngineEditMap" owl="1">
|
||||
<div class="ol_map" t-att-id="id" />
|
||||
</t>
|
||||
</templates>
|
||||
Loading…
Add table
Add a link
Reference in a new issue