mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-19 19:12:00 +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
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
|
|
@ -0,0 +1,659 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>README.rst</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document">
|
||||
|
||||
|
||||
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
|
||||
</a>
|
||||
<div class="section" id="geospatial-support-for-odoo">
|
||||
<h1>Geospatial support for Odoo</h1>
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:2faa9b7de06a2c3c20d9bc960073bdc6638396bc987e338093331e0099278604
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/geospatial/tree/16.0/base_geoengine"><img alt="OCA/geospatial" src="https://img.shields.io/badge/github-OCA%2Fgeospatial-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/geospatial-16-0/geospatial-16-0-base_geoengine"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/geospatial&target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>GeoEngine is an Odoo module that adds spatial/GIS capabilites to Odoo. It will allow you to :</p>
|
||||
<ul class="simple">
|
||||
<li>Visualize and query your business information on map</li>
|
||||
<li>Perform GeoBI and spatial query</li>
|
||||
<li>Configure your spatial layers and spatial datasources</li>
|
||||
<li>Extend Odoo models with spatial columns</li>
|
||||
</ul>
|
||||
<p>GeoEngine relies on <a class="reference external" href="http://openlayers.org">OpenLayers</a> and <a class="reference external" href="http://postgis.refractions.net/">PostgGIS</a> technologies.</p>
|
||||
<p>Postgis is used to store spatial information in databases. OpenLayer is used to represent spatial data in other words to show maps
|
||||
and the different spatial layers. The GeoEngine module acts as a data provider and as an OpenLayers configurator.
|
||||
It also provides a complete extension to Odoo ORM.</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#installation" id="toc-entry-1">Installation</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
|
||||
<li><a class="reference internal" href="#geoengine-demo" id="toc-entry-3">Geoengine Demo</a></li>
|
||||
<li><a class="reference internal" href="#geoengine-backend" id="toc-entry-4">Geoengine Backend</a></li>
|
||||
<li><a class="reference internal" href="#changelog" id="toc-entry-5">Changelog</a></li>
|
||||
<li><a class="reference internal" href="#section-1" id="toc-entry-6">16.0.1.0.0 (2023-03-20)</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-7">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-8">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-9">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-10">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-11">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="installation">
|
||||
<h2><a class="toc-backref" href="#toc-entry-1">Installation</a></h2>
|
||||
<p>To install this module, you need to have <a class="reference external" href="http://postgis.net/">PostGIS</a> installed.</p>
|
||||
<p>On Ubuntu:</p>
|
||||
<pre class="literal-block">
|
||||
.. code-block:: bash
|
||||
</pre>
|
||||
<blockquote>
|
||||
sudo apt-get install postgis</blockquote>
|
||||
<p>The module also requires two additional python libs:</p>
|
||||
<ul class="simple">
|
||||
<li><a class="reference external" href="http://pypi.python.org/pypi/Shapely">Shapely</a></li>
|
||||
<li><a class="reference external" href="http://pypi.python.org/pypi/geojson">geojson</a></li>
|
||||
</ul>
|
||||
<p>When you will install the module this two additional libs will be installed.</p>
|
||||
<p>For a complete documentation please refer to the <a class="reference external" href="http://oca.github.io/geospatial/index.html">public documenation</a></p>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h2><a class="toc-backref" href="#toc-entry-2">Usage</a></h2>
|
||||
</div>
|
||||
<div class="section" id="geoengine-demo">
|
||||
<h2><a class="toc-backref" href="#toc-entry-3">Geoengine Demo</a></h2>
|
||||
<ol class="arabic simple">
|
||||
<li>As a user/admin, when I am in the Geoengine Demo module and I go to the ZIP menu.
|
||||
When I click on an item in the list view, I get to the form view showing me the different
|
||||
information about the ZIP. We can see its ZIP, city, priority, total sales and his spatial
|
||||
representation.</li>
|
||||
<li>As a user, I can’t modify the information in the form view.</li>
|
||||
<li>As an admin, I can modify the information in the form view. I can click on the bin button to clear
|
||||
the map and I can draw a new shape.</li>
|
||||
<li>As a user, when I go the “Retail machines” tab and there are no items to display, it does not
|
||||
show me anything.</li>
|
||||
<li>As an admin, when I go the “Retail machines” tab and there are no items to display, the list view of
|
||||
the retail machines suggests to me to add a new line.</li>
|
||||
<li>As a user/admin, if there are items to be displayed in the “Retail machines” tab then I can click on an
|
||||
item and the retail machines form view will be displayed. We can see its spatial representation by going
|
||||
to “The point” tab and its attributes in “Attributes” tab.</li>
|
||||
<li>As a user/admin, when I go to the geoengine zip view by clicking on the map button at the top right of the
|
||||
screen. The geoengine view appears with the first 80 results displayed on the map. The vector layers
|
||||
selected are those defined as “active on startup” by the admin. The selected raster layer is the first
|
||||
one that is not an overlay layer.</li>
|
||||
<li>As a user/admin, when I hover over an area on the map, the area changes its style.</li>
|
||||
<li>As a user/admin, when I click on an area, a popup appears an I can see the different information about the
|
||||
area. If I click on the cross, the popup will disappear. If I click somewhere else on the map, the
|
||||
popup will also disappear. If I click on the about button, then the form view will be displayed.</li>
|
||||
<li>As a user/admin, when I use the paging system, then the results displayed on the map are different
|
||||
(corresponding to the request).</li>
|
||||
<li>As a user/admin, if we use the search bar, we can search results by his zip or his city.</li>
|
||||
<li>As an admin, if I change the sequence of layers with the handle button then the change are persisted in database.</li>
|
||||
<li>As a user, if I change the sequence of layers with the handle button then the change are not persisted in database.
|
||||
There are just the changes in the display.</li>
|
||||
<li>As an admin, if I change the domain of a layer with the filter button then the change are persisted in database.</li>
|
||||
<li>As a user, if I change the domain of a layer with the filter button then the change are not persisted in database.
|
||||
There are just the changes in the display.</li>
|
||||
<li>As an admin, I have the possibility to edit the layer with its corresponding button.</li>
|
||||
<li>As a user/admin, I can open/close LayerPanel with its button.</li>
|
||||
<li>As a user/admin, I can open/close RecordsPanel with its button.</li>
|
||||
<li>As a user/admin, when I click on a record in RecordsPanel, a move is made on the map to the selected record.</li>
|
||||
<li>As a user/admin, when I click on a record in RecordsPanel, I can also click on the left magnifying glass to zoom on the record.</li>
|
||||
<li>As a user/admin, when I click on a record in RecordsPanel, I can also click on the right magnifying glass to get the original zoom.</li>
|
||||
<li>As a user/admin, I can use the search bar to search in the RecordsPanel.</li>
|
||||
<li>As an admin,If the geoengine view is in edit mode, I can create new records by drawing them in the view.</li>
|
||||
<li>As an admin, If the geoengine view is in edit mode, I can modify its spatial representation.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="section" id="geoengine-backend">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">Geoengine Backend</a></h2>
|
||||
<ol class="arabic simple">
|
||||
<li>As an admin, if I go into the configuration of the raster layers and it has elements, I can click
|
||||
on one and see its information.</li>
|
||||
<li>As an admin, if I want to create a new raster layer, I can click on “NEW” and fill out the form. The
|
||||
required fields for OpenStreetMap type are “Layer Name” and “Related View”. If we want to have a
|
||||
WMTS (Web Map Tile Service) raster type. The required fields in addition to the precedents are “Service URL”,
|
||||
“Matrix set”,”Format”, “Projection” and “Resolutions”. If we take WMS (Web Map Service) raster type, then the
|
||||
required fields are “Layer Name”, “Related View”, “Service URL”, “Params”, “Server Type”.</li>
|
||||
<li>As an admin,if I go into the configuration of the vector layers and it has elements, I can click
|
||||
on one and see its information.</li>
|
||||
<li>As an admin, if I want to create a new vector layer, I can click on “NEW” and fill out the form. The
|
||||
required fields are “Layer Name”, “Related View”, “Geo field” and “Representation mode”.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="section" id="changelog">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">Changelog</a></h2>
|
||||
</div>
|
||||
<div class="section" id="section-1">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">16.0.1.0.0 (2023-03-20)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>LayerSwitcher has been removed as it was not really practical. A LayerPanel is now active.</li>
|
||||
<li>The geo_search method is now deprecated and replaced by the standard odoo search method.</li>
|
||||
<li>The widget “geo_edit_map” attribute is no longer necessary as the field is automatically detected by
|
||||
his type. We can also provide an option attribute that allows us to pass an opacity and a color as
|
||||
parameters.</li>
|
||||
</ul>
|
||||
<pre class="code xml literal-block">
|
||||
<span class="nt"><form></span><span class="w">
|
||||
</span><span class="nt"><notebook</span><span class="w"> </span><span class="na">colspan=</span><span class="s">"4"</span><span class="nt">></span><span class="w">
|
||||
</span><span class="nt"><page</span><span class="w"> </span><span class="na">string=</span><span class="s">"Geometry"</span><span class="nt">></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"the_geom"</span><span class="w"> </span><span class="na">options=</span><span class="s">"{'opacity': 0.8, 'color': '#0000FF' }"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"></page></span><span class="w">
|
||||
</span><span class="nt"></notebook></span><span class="w">
|
||||
</span><span class="nt"></form></span>
|
||||
</pre>
|
||||
<ul class="simple">
|
||||
<li>The method geo_search is now deprecated. We now need to use the standard odoo search method.</li>
|
||||
</ul>
|
||||
<pre class="code python literal-block">
|
||||
<span class="n">obj</span><span class="o">.</span><span class="n">search</span><span class="p">([(</span><span class="s2">"the_point"</span><span class="p">,</span><span class="s2">"geo_intersect"</span><span class="p">,{</span><span class="s2">"dummy.zip.the_geom"</span><span class="p">:</span> <span class="p">[(</span><span class="s2">"id"</span><span class="p">,</span> <span class="s2">"="</span><span class="p">,</span> <span class="n">rec</span><span class="o">.</span><span class="n">id</span><span class="p">)]})])</span>
|
||||
</pre>
|
||||
<ul class="simple">
|
||||
<li>We can now pass to the geoengine view a template to display the information we want
|
||||
to see when clicking on a feature.</li>
|
||||
</ul>
|
||||
<pre class="code xml literal-block">
|
||||
<span class="nt"><geoengine></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"name"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"city"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"total_sales"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"the_geom"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><templates></span><span class="w">
|
||||
</span><span class="nt"><t</span><span class="w"> </span><span class="na">t-name=</span><span class="s">"info_box"</span><span class="nt">></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"city"</span><span class="w"> </span><span class="na">widget=</span><span class="s">"badge"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><ul></span><span class="w">
|
||||
</span><span class="nt"><li></span>ZIP<span class="w"> </span>:<span class="w"> </span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"name"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"></li></span><span class="w">
|
||||
</span><span class="nt"><li></span>Total<span class="w"> </span>Sales:<span class="w"> </span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"total_sales"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"></li></span><span class="w">
|
||||
</span><span class="nt"></ul></span><span class="w">
|
||||
</span><span class="nt"></t></span><span class="w">
|
||||
</span><span class="nt"></templates></span><span class="w">
|
||||
</span><span class="nt"></geoengine></span>
|
||||
</pre>
|
||||
<ul class="simple">
|
||||
<li>We can now pass a model to use to a layer to display other information on the map.</li>
|
||||
</ul>
|
||||
<pre class="code xml literal-block">
|
||||
<span class="nt"><record</span><span class="w"> </span><span class="na">id=</span><span class="s">"geoengine_vector_layer_hs_retail_machines"</span><span class="w"> </span><span class="na">model=</span><span class="s">"geoengine.vector.layer"</span><span class="nt">></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"model_id"</span><span class="w"> </span><span class="na">ref=</span><span class="s">"base_geoengine_demo.model_geoengine_demo_automatic_retailing_machine"</span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"model_domain"</span><span class="nt">></span>[('state',<span class="w"> </span>'=',<span class="w"> </span>'hs')]<span class="nt"></field></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"geo_field_id"</span><span class="w"> </span><span class="na">ref=</span><span class="s">"base_geoengine_demo.field_geoengine_demo_automatic_retailing_machine__the_point"</span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"name"</span><span class="nt">></span>HS<span class="w"> </span>retail<span class="w"> </span>machines<span class="nt"></field></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"view_id"</span><span class="w"> </span><span class="na">ref=</span><span class="s">"ir_ui_view_resbetterzipgeoview0"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"geo_repr"</span><span class="nt">></span>basic<span class="nt"></field></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"attribute_field_id"</span><span class="w"> </span><span class="na">ref=</span><span class="s">"base_geoengine_demo.field_geoengine_demo_automatic_retailing_machine__name"</span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"begin_color"</span><span class="nt">></span>#FF0000<span class="nt"></field></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"display_polygon_labels"</span><span class="w"> </span><span class="na">eval=</span><span class="s">"0"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"layer_opacity"</span><span class="nt">></span>0.8<span class="nt"></field></span><span class="w">
|
||||
</span><span class="nt"></record></span>
|
||||
</pre>
|
||||
<ul class="simple">
|
||||
<li>There is some new features in the LayerPanel.</li>
|
||||
</ul>
|
||||
<ol class="arabic simple">
|
||||
<li>If you are logged in as an admin, you have the possibility to edit the layer by clicking on the edit button. This will open a dialog box.
|
||||
Changes will appear in real time on the view.</li>
|
||||
<li>If you are logged in as an admin, you can also change the domain of the layer. If you are logged in as a user, changes will not be
|
||||
persisted in the database. Changes will appear in real time on the view.</li>
|
||||
<li>If you are logged in as an admin, you can also change the sequence of the layers by sliding them over each other. If you are logged in as a user, changes will not be
|
||||
persisted in the database.</li>
|
||||
</ol>
|
||||
<ul class="simple">
|
||||
<li>Widget domain is now implemented for geo field This means that the geo-operators are also implemented and that there is the possibility to add a sub-domain.
|
||||
If we want to add a domain that includes all the records that are displayed in the geoengine view (active_ids). We can use the two new operators :
|
||||
“in active_ids” and “not in active_ids”. These will automatically replace the marker with ids. Note that the widget will indicate that the domain is invalid
|
||||
because of the marker.</li>
|
||||
<li>Creation of the RecordsPanel. This panel allows you to retrieve all active records. You can click on record to get the movement to the selected record.
|
||||
Two magnifying glass are also available. You can click on the left one to zoom on the record. You can click on the right one to get the original zoom.</li>
|
||||
<li>A search bar is also available. It allows you to perform a search into the RecordsPanel.</li>
|
||||
<li>A button to open/close the panels is also available.</li>
|
||||
<li>The module has been translated in French.</li>
|
||||
<li>Now you can now make the geoengine view editable. Simply add editable attribute in the geoengine view.</li>
|
||||
</ul>
|
||||
<pre class="code xml literal-block">
|
||||
<span class="nt"><geoengine</span><span class="w"> </span><span class="na">editable=</span><span class="s">"1"</span><span class="nt">></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"name"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"city"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"total_sales"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"the_geom"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"display_name"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><templates></span><span class="w">
|
||||
</span><span class="nt"><t</span><span class="w"> </span><span class="na">t-name=</span><span class="s">"info_box"</span><span class="nt">></span><span class="w">
|
||||
</span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"city"</span><span class="w"> </span><span class="na">widget=</span><span class="s">"badge"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"><ul></span><span class="w">
|
||||
</span><span class="nt"><li></span>ZIP<span class="w"> </span>:<span class="w"> </span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"name"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"></li></span><span class="w">
|
||||
</span><span class="nt"><li></span>Total<span class="w"> </span>Sales:<span class="w"> </span><span class="nt"><field</span><span class="w"> </span><span class="na">name=</span><span class="s">"total_sales"</span><span class="w"> </span><span class="nt">/></span><span class="w">
|
||||
</span><span class="nt"></li></span><span class="w">
|
||||
</span><span class="nt"></ul></span><span class="w">
|
||||
</span><span class="nt"></t></span><span class="w">
|
||||
</span><span class="nt"></templates></span><span class="w">
|
||||
</span><span class="nt"></geoengine></span><span class="w">
|
||||
|
||||
</span>Thanks<span class="w"> </span>to<span class="w"> </span>that,<span class="w"> </span>you<span class="w"> </span>can<span class="w"> </span>create<span class="w"> </span>new<span class="w"> </span>records<span class="w"> </span>by<span class="w"> </span>drawing<span class="w"> </span>them<span class="w"> </span>directly<span class="w"> </span>in<span class="w"> </span>the<span class="w"> </span>geoengine<span class="w"> </span>view.<span class="w"> </span>You<span class="w"> </span>can<span class="w"> </span>also<span class="w"> </span>edit<span class="w"> </span>record<span class="w"> </span>in<span class="w"> </span>the<span class="w"> </span>same<span class="w"> </span>view.
|
||||
</pre>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">Bug Tracker</a></h2>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/geospatial/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/geospatial/issues/new?body=module:%20base_geoengine%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">Credits</a></h2>
|
||||
<div class="section" id="authors">
|
||||
<h3><a class="toc-backref" href="#toc-entry-9">Authors</a></h3>
|
||||
<ul class="simple">
|
||||
<li>Camptocamp</li>
|
||||
<li>ACSONE SA/NV</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h3><a class="toc-backref" href="#toc-entry-10">Contributors</a></h3>
|
||||
<ul class="simple">
|
||||
<li>Nicolas Bessi <<a class="reference external" href="mailto:nicolas.bessi@camptocamp.com">nicolas.bessi@camptocamp.com</a>></li>
|
||||
<li>Frederic Junod <<a class="reference external" href="mailto:frederic.junod@camptocamp.com">frederic.junod@camptocamp.com</a>></li>
|
||||
<li>Yannick Payot <<a class="reference external" href="mailto:yannick.payot@camptocamp.com">yannick.payot@camptocamp.com</a>></li>
|
||||
<li>Sandy Carter <<a class="reference external" href="mailto:sandy.carter@savoirfairelinux.com">sandy.carter@savoirfairelinux.com</a>></li>
|
||||
<li>Laurent Mignon <<a class="reference external" href="mailto:laurent.mignon@acsone.eu">laurent.mignon@acsone.eu</a>></li>
|
||||
<li>Jonathan Nemry <<a class="reference external" href="mailto:jonathan.nemry@acsone.eu">jonathan.nemry@acsone.eu</a>></li>
|
||||
<li>David Lasley <<a class="reference external" href="mailto:dave@dlasley.net">dave@dlasley.net</a>></li>
|
||||
<li>Daniel Reis <<a class="reference external" href="mailto:dgreis@sapo.pt">dgreis@sapo.pt</a>></li>
|
||||
<li>Matthieu Dietrich <<a class="reference external" href="mailto:matthieu.dietrich@camptocamp.com">matthieu.dietrich@camptocamp.com</a>></li>
|
||||
<li>Alan Ramos <<a class="reference external" href="mailto:alan.ramos@jarsa.com.mx">alan.ramos@jarsa.com.mx</a>></li>
|
||||
<li>Damien Crier <<a class="reference external" href="mailto:damien.crier@camptocamp.com">damien.crier@camptocamp.com</a>></li>
|
||||
<li>Cyril Gaudin <<a class="reference external" href="mailto:cyril.gaudin@camptocamp.com">cyril.gaudin@camptocamp.com</a>></li>
|
||||
<li>Pierre Verkest <<a class="reference external" href="mailto:pverkest@anybox.fr">pverkest@anybox.fr</a>></li>
|
||||
<li>Benjamin Willig <<a class="reference external" href="mailto:benjamin.willig@acsone.eu">benjamin.willig@acsone.eu</a>></li>
|
||||
<li>Devendra Kavthekar <<a class="reference external" href="mailto:dkatodoo@gmail.com">dkatodoo@gmail.com</a>></li>
|
||||
<li>Emanuel Cino <<a class="reference external" href="mailto:ecino@compassion.ch">ecino@compassion.ch</a>></li>
|
||||
<li>Thomas Nowicki <<a class="reference external" href="mailto:thomas.nowicki@camptocamp.com">thomas.nowicki@camptocamp.com</a>></li>
|
||||
<li>Alexandre Saunier <<a class="reference external" href="mailto:alexandre.saunier@camptocamp.com">alexandre.saunier@camptocamp.com</a>></li>
|
||||
<li>Sandip Mangukiya <<a class="reference external" href="mailto:smangukiya@opensourceintegrators.com">smangukiya@opensourceintegrators.com</a>></li>
|
||||
<li>Samuel Kouff <<a class="reference external" href="mailto:s.kouff@student.helmo.be">s.kouff@student.helmo.be</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h3><a class="toc-backref" href="#toc-entry-11">Maintainers</a></h3>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||
</a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/geospatial/tree/16.0/base_geoengine">OCA/geospatial</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 356 B |
|
|
@ -0,0 +1,28 @@
|
|||
chroma.js - JavaScript library for color conversions
|
||||
|
||||
Copyright (c) 2011-2015, Gregor Aisch
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. The name Gregor Aisch may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL GREGOR AISCH OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
||||
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,24 @@
|
|||
.geostats-legend div {
|
||||
margin:3px 10px 5px 10px;
|
||||
clear:left;
|
||||
}
|
||||
|
||||
.geostats-legend-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.geostats-legend-block {
|
||||
border: 1px solid #555555;
|
||||
display: block;
|
||||
float: left;
|
||||
height: 12px;
|
||||
margin: 0 5px 0 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.geostats-legend-counter {
|
||||
font-size: 0.8em;
|
||||
color:#666;
|
||||
font-style: italic;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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