Skip to content

Commit

Permalink
add layer-intersect tool
Browse files Browse the repository at this point in the history
  • Loading branch information
anneb committed Jan 22, 2023
1 parent eb981ec commit fc5d511
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 114 deletions.
8 changes: 7 additions & 1 deletion src/components/map-data-toolbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import {LitElement, html, svg, css} from 'lit';
import './map-iconbutton';
import './map-datatool-distance';
import './map-datatool-buffer';
import './map-datatool-intersect';
import './map-iconbutton';
import {bufferIcon} from './my-icons';
import {bufferIcon, intersectIcon} from './my-icons';
import { measureIcon} from '../gm/gm-iconset-svg';

//const dummyIcon = svg`<svg height="24" width="24" viewbox="0 0 24 24"><style>.normal{ font: bold 18px sans-serif;}</style><text x="4" y="16" class="normal">A</text></svg>`;
Expand Down Expand Up @@ -50,6 +51,9 @@ class MapDataToolbox extends LitElement {
<div class="tool">
<map-iconbutton .active="${this.currentTool==='buffertool'}" .icon="${bufferIcon}" info="Bufferen" @click="${e=>this.currentTool='buffertool'}"></map-iconbutton>
</div>
<div class="tool">
<map-iconbutton .active="${this.currentTool==='intersecttool'}" .icon="${intersectIcon}" info="Overlap" @click="${e=>this.currentTool='intersecttool'}"></map-iconbutton>
</div>
</div>
<div class="toolpanel">
${this._renderCurrentTool()}
Expand All @@ -65,6 +69,8 @@ class MapDataToolbox extends LitElement {
return html`<map-datatool-distance .map=${this.map}></map-datatool-distance>`;
case "buffertool":
return html`<map-datatool-buffer @titlechange="${()=>this._titlechange()}" .map=${this.map}></map-datatool-buffer>`;
case "intersecttool":
return html`<map-datatool-intersect @titlechange="${()=>this._titlechange()}" .map=${this.map}></map-datatool-intersect>`;
default:
return html`Nog niet geimplementeerd: '${this.currentTool}'`;
}
Expand Down
109 changes: 2 additions & 107 deletions src/components/map-datatool-buffer.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import {LitElement, html, css} from 'lit';
import {customSelectCss} from './custom-select-css.js';
import {SphericalMercator} from '../lib/sphericalmercator.js';
import Flatbush from 'flatbush';
import 'jsts/org/locationtech/jts/monkey.js';
import GeoJSONReader from 'jsts/org/locationtech/jts/io/GeoJSONReader';
import GeoJSONWriter from 'jsts/org/locationtech/jts/io/GeoJSONWriter';
import getVisibleFeatures from '../utils/mbox-features';
import './wc-button';
import {GeoJSON} from '../utils/geojson';
import lineUnion from '@edugis/lineunion';

class MapDatatoolBuffer extends LitElement {
static get styles() {
Expand Down Expand Up @@ -144,108 +138,9 @@ class MapDatatoolBuffer extends LitElement {

return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
featurePropertiesAreEqual(feature1, feature2) {
for (const key in feature1.properties) {
if (!key in feature2) {
return false;
}
if (feature1.properties[key] !== feature2.properties[key]) {
return false;
}
}
for (const key in feature2.properties) {
if (!key in feature1) {
return false;
}
}
return true;
}
async _getVisibleFeatures(layerid) {
const layer = this.map.getLayer(layerid);
const mapBounds = this.map.getBounds();
if (!layer.sourceLayer && layer.type !== 'circle' && layer.type !== 'symbol') {
// not a vector tile layer or circle layer or symbol layer
const source = this.map.getSource(layer.source).serialize();
if (typeof source.data === "string") {
const response = await fetch(source.data);
if (response.ok) {
source.data = await response.json();
}
}
const features = source.data.features.filter(feature=>{
const bbox = turf.bbox(feature);
return bbox[0] < mapBounds._ne.lng && bbox[2] > mapBounds._sw.lng && bbox[1] < mapBounds._ne.lat && bbox[3] > mapBounds._sw.lat;
});
return features;
} else {
const tileMap = new Map();
const sphericalmercator = new SphericalMercator();
const tileBorderFeatures = [];
let features = this.map.queryRenderedFeatures(undefined,{layers:[layerid]}).map((mboxfeature, index)=>{
const x = mboxfeature._vectorTileFeature._x;
const y = mboxfeature._vectorTileFeature._y;
const z = mboxfeature._vectorTileFeature._z;
const xyz = `${x},${y},${z}`;
let tileBBox = tileMap.get(xyz)
if (!tileBBox) {
tileBBox = sphericalmercator.bbox(x, y, z);
tileMap.set(xyz, tileBBox);
}
const jsonFeature = mboxfeature.toJSON();
const featureBBox = turf.bbox(jsonFeature);
if ((featureBBox[0] < tileBBox[0] || featureBBox[2] > tileBBox[2] || featureBBox[1] < tileBBox[1] || featureBBox[3] > tileBBox[3])) {
tileBorderFeatures.push({index:index, bbox: featureBBox});
}
return jsonFeature;
});

if (tileBorderFeatures.length) {
const flatbushIndex = new Flatbush(tileBorderFeatures.length);
for (const featureInfo of tileBorderFeatures) {
const bbox = featureInfo.bbox;
flatbushIndex.add(bbox[0], bbox[1], bbox[2], bbox[3]);
}
flatbushIndex.finish();
const firstTileBBox = tileMap.entries().next().value[1];
const tolerance = (firstTileBBox[2] - firstTileBBox[0]) / 5000;

for (let i = 0; i < tileBorderFeatures.length; i++) {
const featureIndex = tileBorderFeatures[i].index;
let feature1 = features[featureIndex];
if (feature1 === null) {
continue;
}
const bbox = tileBorderFeatures[i].bbox;
const intersectCandidates = flatbushIndex.search(bbox[0], bbox[1], bbox[2], bbox[3]);
for (let j = 0; j < intersectCandidates.length; j++) {
const feature2Index = tileBorderFeatures[intersectCandidates[j]].index;
if (featureIndex !== feature2Index) {
const feature2 = features[feature2Index];
if (feature2 === null) {
continue;
}
if (feature1 && feature2 && turf.booleanIntersects(feature1, feature2)) {
if (this.featurePropertiesAreEqual(feature1, feature2)) {
if (feature1.geometry.type === "LineString" || feature1.geometry.type === "MultiLineString") {
feature1 = features[featureIndex] = lineUnion(feature1, feature2, tolerance);
} else {
feature1 = features[featureIndex] = turf.union(feature1, feature2);
}
features[tileBorderFeatures[intersectCandidates[j]].index] = null;
tileBorderFeatures[intersectCandidates[j]].index = featureIndex;
}
}
}
}
}
features=features.filter(feature=>feature !== null);
}
return features;
}
}
async _calculateBuffer(layerid) {
// buffer is calculated for currently visible elements only
const vectorFeatures = await this._getVisibleFeatures(layerid);
const vectorFeatures = await getVisibleFeatures(this.map, layerid);
//const vectorFeatures = this.map.queryRenderedFeatures(undefined,{layers:[layerid]});
if (vectorFeatures.length) {
let buffervalue = this.shadowRoot.querySelector('#targetbuffer').value.replace(",",".");
Expand Down
99 changes: 99 additions & 0 deletions src/components/map-datatool-intersect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {LitElement, html, css} from 'lit';
import { layerIntersect } from '../utils/layerintersect';
import getVisibleFeatures from '../utils/mbox-features';
import './wc-button';

class MapDatatoolIntersect extends LitElement {
static get styles() {
return css`
:host {
display: block;
}`
}
static get properties() {
return {
resulttype: {type: String},
map: {type: Object},
intersectCount: {type: Number}
};
}
constructor() {
super();
this.resulttype = 'aantal';
this.map = {};
this.intersectCount = -1;
}
connectedCallback() {
super.connectedCallback()
//addEventListener('keydown', this._handleKeydown);
}
disconnectedCallback() {
super.disconnectedCallback()
//window.removeEventListener('keydown', this._handleKeydown);
}
shouldUpdate(changedProp) {
if (changedProp.has('sprop')) {
// do something with sprop change
}
return true;
}
render() {
return html`
<b>Intersect berekenen</b><p></p>
Bereken elementen in kaartlaag 1 die een element in kaartlaag2 snijden (ergens raken)<p></p>
<b>Kaartlaag 1</b><br>
${this._renderLayerList()}
<b>Kaartlaag 2</b><br>
${this._renderLayerList()}
<input type="radio" name="resulttype" value="aantal" id="aantal" ?checked="${this.resulttype==='aantal'}"><label for="aantal">Aantal</label><br>
<input type="radio" name="resulttype" value="layeryes" id="layeryes" ?checked="${this.resulttype==='layeryes'}"><label for="layeryes">Uitvoerkaartlaag met elementen met een overlap</label><br>
<input type="radio" name="resulttype" value="layerno" id="layerno" ?checked="${this.resulttype==='layerno'}"><label for="layerno">Uitvoerkaartlaag met elementen zonder een overlap</label><br>
<wc-button class="edugisblue" @click="${e=>this._handleClick(e)}" ?disabled="${!this.buttonEnabled}">Berekenen</wc-button><br>
${this.intersectCount > -1 ?html`Aantal elementen: ${this.intersectCount}`:html``}
</div>
`
}
firstUpdated() {

}
updated() {

}
_layerSelected(e) {
const selections = this.shadowRoot.querySelectorAll('select');
this.buttonEnabled = (selections.length === 2 && (selections[0].value != selections[1].value) && (selections[0].value !== '') && selections[1].value !== '');
this.sourceLayerid = this.buttonEnabled ? selections[0].value : undefined;
this.targetLayerid = this.buttonEnabled ? selections[1].value : undefined;
this.intersectCount = -1;
this.update();
}
_renderLayerList() {
const layers = this.map.getStyle().layers.filter(layer=>layer.metadata && !layer.metadata.reference && !layer.metadata.isToolLayer && ['fill','line','circle','symbol'].includes(layer.type));
if (layers.length < 2) {
return html`${layers.length} kaartlagen aanwezig (minimmaal 2 nodig)`;
}
return html`<div class="styled-select"><select @change="${e=>this._layerSelected(e)}">
<option value="" disabled selected>Selecteer kaartlaag</option>
${layers.map(layer=>html`<option value=${layer.id}>${layer.metadata.title?layer.metadata.title:layer.id}</option>`)}
</select><span class="arrow"></span></div>`
}
async _calculateIntersect() {
if (this.sourceLayerid && this.targetLayerid) {
const sourceFeatures = await getVisibleFeatures(this.map, this.sourceLayerid);
const targetFeatures = await getVisibleFeatures(this.map, this.targetLayerid);
const intersectLayer = layerIntersect({type: "FeatureCollection", features:sourceFeatures}, {type:"FeatureCollection", features:targetFeatures});
this.intersectCount = intersectLayer.features.reduce((total, feature)=>{
return feature.properties.intersect?total+1:total
}, 0);
this.update();
}
}
_handleClick(e) {
if (!this.buttonEnabled) {
return;
}
this._calculateIntersect();
}
}

customElements.define('map-datatool-intersect', MapDatatoolIntersect);
38 changes: 37 additions & 1 deletion src/components/my-icons.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions src/components/web-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import ZoomControl from '../../lib/zoomcontrol';
import { importExportIcon, gpsIcon, languageIcon, arrowLeftIcon, outlineInfoIcon, combineToolIcon, threeDIcon, infoIcon, drawIcon, sheetIcon, world3Icon } from './my-icons';
import { measureIcon, layermanagerIcon, searchIcon as gmSearchIcon } from '../gm/gm-iconset-svg';
import rootUrl from '../utils/rooturl.js';
import {geoJSONProject} from '@edugis/proj-convert'
import {geoJSONProject, coordProject} from '@edugis/proj-convert'

function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
Expand Down Expand Up @@ -140,10 +140,10 @@ function projectLngLat(lngLat, srs)
if (!srs) {
srs = 'EPSG:3857';
}
var project = proj4('EPSG:4326', srs);
var p = project.forward({x: lngLat.lng, y: lngLat.lat});
lngLat.x = p.x;
lngLat.y = p.y;
var p = coordProject([lngLat.lng, lngLat.lat], 'EPSG:4326', srs);

lngLat.x = p[0];
lngLat.y = p[1];
return lngLat;
}

Expand Down
22 changes: 22 additions & 0 deletions src/utils/clonejson.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function cloneJSON(obj) {
// basic type deep copy
if (obj === null || obj === undefined || typeof obj !== 'object') {
return obj
}
// array deep copy
if (obj instanceof Array) {
var cloneA = [];
for (var i = 0; i < obj.length; ++i) {
cloneA[i] = cloneJSON(obj[i]);
}
return cloneA;
}
// object deep copy
var cloneO = {};
for (var i in obj) {
cloneO[i] = cloneJSON(obj[i]);
}
return cloneO;
}

export default cloneJSON;
37 changes: 37 additions & 0 deletions src/utils/layerintersect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Flatbush from 'flatbush';
import cloneJSON from '../utils/clonejson';

function flatBushIndex(layer) {
const flatbushIndex = new Flatbush(layer.features.length);
for (const feature of layer.features) {
const bbox = turf.bbox(feature);
flatbushIndex.add(bbox[0], bbox[1], bbox[2], bbox[3]);
}
flatbushIndex.finish();
return flatbushIndex;
}

export function layerIntersect(layer1, layer2) {
if (layer1 && layer1.type && layer1.type === "FeatureCollection" && layer2 && layer2.type && layer2.type === "FeatureCollection") {
// create index on layer2
const layerIndex = flatBushIndex(layer2);
layer1 = cloneJSON(layer1);
for (const feature of layer1.features) {
const bbox = turf.bbox(feature);
const intersectCandidates = layerIndex.search(bbox[0], bbox[1], bbox[2], bbox[3]);
for (const candidate of intersectCandidates) {
const feature2 = layer2.features[candidate];
if (turf.booleanIntersects(feature, feature2)) {
feature.properties.intersect = true;
break;
}
}
}
}
for (const feature of layer1.features) {
if (!feature.properties.intersect) {
feature.properties.intersect = false;
}
}
return layer1;
}
Loading

0 comments on commit fc5d511

Please sign in to comment.