Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions spp_gis/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ class MainController(http.Controller):
def get_maptiler_api_key(self):
# nosemgrep: odoo-sudo-without-context
map_tiler_api_key = request.env["ir.config_parameter"].sudo().get_param("spp_gis.map_tiler_api_key")
# Treat the default placeholder as "not configured"
if map_tiler_api_key == "YOUR_MAPTILER_API_KEY_HERE":
map_tiler_api_key = False
# nosemgrep: odoo-sudo-without-context
web_base_url = request.env["ir.config_parameter"].sudo().get_param("web.base.url")
return {"mapTilerKey": map_tiler_api_key, "webBaseUrl": web_base_url}
27 changes: 25 additions & 2 deletions spp_gis/operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class Operator:
"Point": "point",
"LineString": "line",
"Polygon": "polygon",
"MultiPolygon": "multipolygon",
"GeometryCollection": "geometrycollection",
}

def __init__(self, field, table_alias=None):
Expand Down Expand Up @@ -256,6 +258,18 @@ def create_polygon(self, coordinates, srid):
polygon = self.st_makepolygon(points)
return self.st_setsrid(polygon, srid)

def create_from_geojson(self, geojson_dict, srid):
"""Create geometry from full GeoJSON using ST_GeomFromGeoJSON.

Used for complex geometry types (MultiPolygon, GeometryCollection)
that cannot be easily constructed from coordinates.

Returns a SQL object with the GeoJSON string as a bound parameter
to avoid SQL injection via string interpolation.
"""
geojson_str = json.dumps(geojson_dict)
return SQL("ST_SetSRID(ST_GeomFromGeoJSON(%s), %s)", geojson_str, srid)

def validate_coordinates_for_point(self, coordinates):
"""
The function `validate_coordinates_for_point` checks if a set of coordinates represents a valid
Expand Down Expand Up @@ -454,7 +468,10 @@ def validate_geojson(self, geojson):
to validate the structure of the GeoJSON using the `shape` function
"""
if geojson.get("type") not in self.ALLOWED_LAYER_TYPE:
raise ValueError("Invalid geojson type. Allowed types are Point, LineString, and Polygon.")
raise ValueError(
"Invalid geojson type. Allowed types are Point, LineString, "
"Polygon, MultiPolygon, and GeometryCollection."
)
try:
shape(geojson)
except Exception as e:
Expand Down Expand Up @@ -487,6 +504,12 @@ def domain_query(self, operator, value):

operation = self.OPERATION_TO_RELATION[operator]
layer_type = self.ALLOWED_LAYER_TYPE[geojson_val["type"]]
coordinates = geojson_val["coordinates"]

if layer_type in ("multipolygon", "geometrycollection"):
# Complex types use ST_GeomFromGeoJSON directly
geom = self.create_from_geojson(geojson_val, self.field.srid)
postgis_fn = self.POSTGIS_SPATIAL_RELATION[operation]
return SQL("%s(%s, %s)", SQL(postgis_fn), geom, SQL(self.qualified_field_name))

coordinates = geojson_val["coordinates"]
return SQL(self.get_postgis_query(operation, coordinates, distance=distance, layer_type=layer_type))
42 changes: 34 additions & 8 deletions spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ export class GisRenderer extends Component {
});

onMounted(() => {
maptilersdk.config.apiKey = this.mapTilerKey;
if (this.mapTilerKey) {
maptilersdk.config.apiKey = this.mapTilerKey;
}
this.setupSourceAndLayer();

this.renderMap();
Expand All @@ -106,14 +108,11 @@ export class GisRenderer extends Component {
async getMapTilerKey() {
try {
const response = await this.rpc("/get_maptiler_api_key");
this.mapTilerKey = response.mapTilerKey;
if (response.mapTilerKey) {
this.mapTilerKey = response.mapTilerKey;
} else {
console.log("Error: Api Key not found.");
}
} catch (error) {
console.error("Error fetching environment variable:", error);
console.warn("Could not fetch MapTiler API key:", error);
}
}

Expand Down Expand Up @@ -416,7 +415,7 @@ export class GisRenderer extends Component {

let defaultMapStyle = this.getMapStyle();

if (this.defaultRaster) {
if (this.mapTilerKey && this.defaultRaster) {
if (this.defaultRaster.raster_style.includes("-")) {
const rasterStyleArray = this.defaultRaster.raster_style
.toUpperCase()
Expand Down Expand Up @@ -459,11 +458,38 @@ export class GisRenderer extends Component {

this.addMouseInteraction();

const gc = new maptilersdkMaptilerGeocoder.GeocodingControl({});
this.map.addControl(gc, "top-left");
if (this.mapTilerKey) {
const gc = new maptilersdkMaptilerGeocoder.GeocodingControl({});
this.map.addControl(gc, "top-left");
}
}

getMapStyle(layer) {
if (!this.mapTilerKey) {
// Fallback: OSM raster tiles (no API key required)
return {
version: 8,
sources: {
osm: {
type: "raster",
tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256,
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
},
},
layers: [
{
id: "osm-tiles",
type: "raster",
source: "osm",
minzoom: 0,
maxzoom: 19,
},
],
};
}

let mapStyle = maptilersdk.MapStyle.STREETS;

if (layer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export class FieldGisEditMap extends Component {
});

onMounted(async () => {
maptilersdk.config.apiKey = this.mapTilerKey;
if (this.mapTilerKey) {
maptilersdk.config.apiKey = this.mapTilerKey;
}
const editInfo = await this.orm.call(
this.props.record.resModel,
"get_edit_info_for_gis_column",
Expand Down Expand Up @@ -67,11 +69,9 @@ export class FieldGisEditMap extends Component {
if (response.mapTilerKey) {
this.mapTilerKey = response.mapTilerKey;
this.webBaseUrl = response.webBaseUrl;
} else {
console.log("Error: Api Key not found.");
}
} catch (error) {
console.error("Error fetching environment variable:", error);
console.warn("Could not fetch MapTiler API key:", error);
}
}

Expand All @@ -86,6 +86,34 @@ export class FieldGisEditMap extends Component {
}
}

_getMapStyle() {
if (this.mapTilerKey) {
return maptilersdk.MapStyle.STREETS;
}
// Fallback: OSM raster tiles (no API key required)
return {
version: 8,
sources: {
osm: {
type: "raster",
tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256,
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
},
},
layers: [
{
id: "osm-tiles",
type: "raster",
source: "osm",
minzoom: 0,
maxzoom: 19,
},
],
};
}
Comment on lines +89 to +115

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The code to generate the fallback OpenStreetMap map style object is duplicated from spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js. To improve maintainability and avoid inconsistencies, this object could be defined as a constant or returned by a utility function in a shared file, and then imported in both field_gis_edit_map.esm.js and gis_renderer.esm.js.


renderMap() {
if (this.props.record.data[this.props.name]) {
const obj = JSON.parse(this.props.record.data[this.props.name]);
Expand All @@ -104,9 +132,14 @@ export class FieldGisEditMap extends Component {
this.defaultZoom = 10;
}

if (this.map) {
this.draw = null;
this.map.remove();
}

this.map = new maptilersdk.Map({
container: this.id,
style: maptilersdk.MapStyle.STREETS,
style: this._getMapStyle(),
center: this.defaultCenter,
zoom: this.defaultZoom,
});
Expand Down Expand Up @@ -199,7 +232,9 @@ export class FieldGisEditMap extends Component {
}

removeSourceAndLayer(source) {
this.map.removeLayer(source);
this.map.removeLayer(`${source}-polygon-layerid`);
this.map.removeLayer(`${source}-point-layerid`);
this.map.removeLayer(`${source}-linestring-layerid`);
this.map.removeSource(source);
}

Expand All @@ -213,13 +248,16 @@ export class FieldGisEditMap extends Component {
const self = this;

function updateArea(e) {
console.log(e);
var data = self.draw.getAll();
self.props.record.update({
[self.props.name]: JSON.stringify(data.features[0].geometry),
});
}
Comment on lines 250 to 255

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The updateArea function assumes that self.draw.getAll().features will always contain at least one feature. If it's empty for any reason, data.features[0] will be undefined and accessing .geometry will cause a runtime error. It's safer to add a check.

Additionally, the draw.create and draw.update events pass the affected features in the event object e, which is more direct and efficient to use than calling getAll().

        function updateArea(e) {
            const features = e.features;
            if (features && features.length > 0) {
                self.props.record.update({
                    [self.props.name]: JSON.stringify(features[0].geometry),
                });
            }
        }


if (this.draw) {
this.map.removeControl(this.draw);
}

this.draw = new MapboxDraw({
displayControlsDefault: false,
controls: {
Expand All @@ -246,17 +284,6 @@ export class FieldGisEditMap extends Component {

this.map.on("draw.create", updateArea);
this.map.on("draw.update", updateArea);

const url = `/spp_gis/static/src/images/laos_farm.png`;

this.map.on("click", `${this.sourceId}-polygon-layerid`, (e) => {
new maptilersdk.Popup()
.setLngLat(e.lngLat)
.setHTML(
`<img src="${url}" height="200" width="300" alt="Placeholder Image">`
)
.addTo(this.map);
});
}

addDrawInteractionStyle() {
Expand Down Expand Up @@ -370,7 +397,6 @@ export class FieldGisEditMap extends Component {
const customMode = {};
const self = this;
customMode.onTrash = function (state) {
console.log(state);
self.props.record.update({[self.props.name]: null});
};

Expand Down
Loading
Loading