Merge branch '114-allow-users-to-show-arbitrary-geojson-on-the-map' into 'devel'

Resolve "Allow users to show arbitrary GeoJSON on the map."

Closes #114

See merge request wgp/dougal/software!37
This commit is contained in:
D. Berge
2023-09-12 17:34:51 +00:00
10 changed files with 316 additions and 6 deletions

View File

@@ -375,7 +375,8 @@ export default {
}
],
labels: {},
hashMarker: null
hashMarker: null,
references: {}
};
},
@@ -474,7 +475,7 @@ export default {
bounds._northEast.lng,
bounds._northEast.lat
].map(i => i.toFixed(bboxScale)).join(",");
const limit = 10000;
const limit = 10000; // Empirical value
const query = new URLSearchParams({bbox, limit});
@@ -520,7 +521,9 @@ export default {
l.layer.lastRequestURL = url;
} else {
console.warn("Too much data from", url);
console.warn(`Too much data from ${url} (${layer.features.length ?? layer.length}${limit} features)`);
this.showSnack([`Layer ${l.layer.options.userLayerName} is too large: ${layer.features.length ?? layer.length} features; maximum is ${limit}`, "error"]);
}
})
.finally( () => {
@@ -677,7 +680,109 @@ export default {
this.labels = await this.api([url]) || [];
},
...mapActions(["api"])
removeUserLayers () {
map.eachLayer( layer => {
if (layer.options.userLayer === true) {
console.log("Removing", layer);
layer.eachLayer( sublayer => {
const idx = this.layerRefreshConfig.findIndex(i => i.layer == layer);
if (idx != -1) {
this.layerRefreshConfig.splice(idx, 1);
}
});
map.removeLayer(layer);
this.references.layerControl.removeLayer(layer);
}
});
},
async addUserLayers (userLayers) {
const options = {
userLayer: true,
style (feature) {
const style = {
stroke: undefined,
color: "grey",
weight: 2,
opacity: 0.5,
lineCap: undefined,
lineJoin: undefined,
dashArray: undefined,
dashOffset: undefined,
fill: undefined,
fillColor: "lightgrey",
fillOpacity: 0.5,
fillRule: undefined
};
for (let key in style) {
switch (key) {
case "color":
style[key] = feature.properties?.colour ?? feature.properties?.color ?? style[key];
break;
case "fillColor":
style[key] = feature.properties?.fillColour ?? feature.properties?.fillColor ?? style[key];
break;
default:
style[key] = feature.properties?.[key] ?? style[key];
}
if (typeof style[key] === "undefined") {
delete style[key];
}
}
return style;
}
};
const userLayerGroups = {};
userLayers.forEach(layer => {
if (!(layer.name in userLayerGroups)) {
userLayerGroups[layer.name] = [];
}
userLayerGroups[layer.name].push(layer);
});
for (let userLayerName in userLayerGroups) {
const userLayerGroup = userLayerGroups[userLayerName];
const layer = L.featureGroup(null, {userLayer: true, userLayerGroup: true, userLayerName});
userLayerGroup.forEach(l => {
const sublayer = L.geoJSON(null, {...options, userLayerName});
layer.addLayer(sublayer);
sublayer.on('add', ({target}) => {
this.refreshLayers([target])
});
const refreshConfig = {
layer: sublayer,
url: (query = "") => {
return `/files/${l.path}`;
}
};
this.layerRefreshConfig.push(refreshConfig);
});
layer.on('add', ({target}) => {
this.refreshLayers(target.getLayers())
});
this.references.layerControl.addOverlay(layer, `<span title="User layer" style="text-decoration: dotted underline;">${userLayerName}</span>`);
}
},
async fetchUserLayers () {
const url = `/project/${this.$route.params.project}/gis/layer`;
const userLayers = await this.api([url]) || [];
this.removeUserLayers();
this.addUserLayers(userLayers);
},
...mapActions(["api", "showSnack"])
},
@@ -756,6 +861,9 @@ export default {
const layerControl = L.control.layers(tileMaps, layers).addTo(map);
const scaleControl = L.control.scale().addTo(map);
this.references.layerControl = layerControl;
this.references.scaleControl = scaleControl;
if (init.position) {
map.setView(init.position.slice(1), init.position[0]);
} else {
@@ -783,10 +891,13 @@ export default {
map.on('layeradd', this.updateURL);
map.on('layerremove', this.updateURL);
this.layerRefreshConfig.forEach( l => {
l.layer.on('add', ({target}) => this.refreshLayers([target]));
});
this.fetchUserLayers();
if (init.position) {
this.refreshLayers();
} else {

View File

@@ -123,6 +123,12 @@ app.map({
'/project/:project/gis/final/:featuretype(line|point)': {
get: [ mw.gis.project.final ]
},
'/project/:project/gis/layer': {
get: [ mw.etag.noSave, mw.gis.project.layer.get ]
},
'/project/:project/gis/layer/:name': {
get: [ mw.etag.noSave, mw.gis.project.layer.get ]
},
/*
* Line endpoints

View File

@@ -2,5 +2,6 @@ module.exports = {
bbox: require('./bbox'),
preplot: require('./preplot'),
raw: require('./raw'),
final: require('./final')
final: require('./final'),
layer: require('./layer')
};

View File

@@ -0,0 +1,18 @@
const { gis } = require('../../../../../lib/db');
module.exports = async function (req, res, next) {
try {
const layers = await gis.project.layer.get(req.params.project, req.params.name);
if (req.params.name && (!layers || !layers.length)) {
res.status(404).json({message: "Not found"});
} else {
res.status(200).send(layers ?? []);
}
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,3 @@
module.exports = {
get: require('./get')
};

View File

@@ -3,5 +3,6 @@ module.exports = {
bbox: require('./bbox'),
preplot: require('./preplot'),
raw: require('./raw'),
final: require('./final')
final: require('./final'),
layer: require('./layer')
};

View File

@@ -0,0 +1,31 @@
const { setSurvey } = require('../../../connection');
async function get (projectId, layerName = null, options = {}) {
const client = await setSurvey(projectId);
const text = `
SELECT path, (data - 'type') data
FROM files f
INNER JOIN file_data
USING (hash)
WHERE data->>'type' = 'map_layer'
AND data->>'format' = 'geojson'
AND (data->>'name' = $1
OR $1 IS NULL);
`;
const values = [ layerName ];
const res = await client.query(text, values);
client.release();
if (res.rows && res.rows.length) {
return res.rows.map(row => ({...row.data, path: row.path}));
} else {
throw {status: 404, message: "Not found"};
}
}
module.exports = get;

View File

@@ -0,0 +1,4 @@
module.exports = {
get: require('./get')
};