Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,9 @@
{
"globals": {
"$": false,
"_": false,
"ol": false,
"chroma": false,
"geostats": false
}
}

View file

@ -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());

View file

@ -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());

View file

@ -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,
};
}
}

View file

@ -0,0 +1,9 @@
/** @odoo-module */
/**
* Copyright 2023 ACSONE SA/NV
*/
import {ViewCompiler} from "@web/views/view_compiler";
export class GeoengineCompiler extends ViewCompiler {}

View file

@ -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};

View file

@ -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>

View file

@ -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;

View file

@ -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>

View file

@ -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: "";
}

View file

@ -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>

View file

@ -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);

View file

@ -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};

View file

@ -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;
}

View file

@ -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>

View file

@ -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};

View file

@ -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;
}

View file

@ -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>

View file

@ -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";

View file

@ -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>

View file

@ -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});

View file

@ -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";

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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",
};

View file

@ -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>

View file

@ -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};

View file

@ -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>

View file

@ -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});

View file

@ -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>

View file

@ -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 {};
};
}

View file

@ -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%;
}

View file

@ -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);

View file

@ -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>