UNPKG

sparnatural

Version:

Visual client-side SPARQL query builder and knowledge graph exploration tool

472 lines (388 loc) 14.4 kB
import { DataFactory } from 'rdf-data-factory'; // L needs to be imported *before* leaflet-geoman-free import L, { LatLng, Rectangle, PolylineOptions, Polygon, PM, TileLayer } from "leaflet"; import AddUserInputBtn from "../buttons/AddUserInputBtn"; import { AbstractWidget, ValueRepetition, WidgetValue } from "./AbstractWidget"; import { FilterPattern, FunctionCallExpression, LiteralTerm, Pattern, } from "sparqljs"; import "leaflet/dist/leaflet.css"; import "@geoman-io/leaflet-geoman-free"; import "@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css"; import { SelectedVal } from "../SelectedVal"; import { NamedNode } from '@rdfjs/types/data-model'; import { I18n } from '../../settings/I18n'; import HTMLComponent from '../HtmlComponent'; import CriteriaGroup from '../builder-section/groupwrapper/criteriagroup/CriteriaGroup'; const factory = new DataFactory(); const GEOFUNCTIONS_NAMESPACE = 'http://www.opengis.net/def/function/geosparql/' export const GEOFUNCTIONS = { WITHIN: factory.namedNode(GEOFUNCTIONS_NAMESPACE + 'sfWithin') as NamedNode } const GEOSPARQL_NAMESPACE = "http://www.opengis.net/ont/geosparql#" export const GEOSPARQL = { WKT_LITERAL: factory.namedNode(GEOSPARQL_NAMESPACE + 'wktLiteral') as NamedNode } export class MapValue implements WidgetValue { value: { label: string; type: string; coordinates: LatLng[][]; }; key():string { return this.value.coordinates.toString(); } constructor(v:MapValue["value"]) { this.value = v; } exportToGeoJson():object { // TODO : recreate a GeoJSON value from the MapWidgetValue return {}; } } // converts props of type Date to type string type ObjectifyLatLng<T> = T extends LatLng[][] ? [[{lat:number,lng:number}]] : T extends object ? { [k in keyof T]: ObjectifyLatLng<T[k]>; } : T; // stringified type of MapWidgetValue // see: https://effectivetypescript.com/2020/04/09/jsonify/ type ObjectMapWidgetValue = ObjectifyLatLng<MapValue> export interface MapConfiguration { zoom: number, center: { lat: number, long: number } } export interface CustomControlOptions { name: string, block: any, title: string, className: string, onClick: () => void } export default class MapWidget extends AbstractWidget { // The default implementation of MapConfiguration static defaultConfiguration: MapConfiguration = { zoom:5, center: { lat: 46.20222, long: 6.14569 } } protected configuration: MapConfiguration; protected endClassWidgetGroup: any; protected widgetValues: MapValue[]; //protected widgetValue: MapWidgetValue[]; // protected blockObjectPropTriple: boolean = true renderMapValueBtn: AddUserInputBtn; map: L.Map; drawingLayer: L.Layer; constructor( configuration: MapConfiguration, parentComponent: HTMLComponent, startClassVal: SelectedVal, objectPropVal: SelectedVal, endClassVal: SelectedVal ) { super( "map-widget", parentComponent, null, startClassVal, objectPropVal, endClassVal, ValueRepetition.SINGLE ); this.configuration = configuration; this.parentComponent = parentComponent; this.endClassWidgetGroup = (this.parentComponent.parentComponent.parentComponent.parentComponent as CriteriaGroup).endClassWidgetGroup ; } render(): this { console.log("rendering map widget...") console.log(this) ; super.render(); this.renderMapValueBtn = new AddUserInputBtn( this, I18n.labels.MapWidgetOpenMap, this.#renderMap ).render(); // opens the map if there is a value - in the case we are editing the value if(this.widgetValues?.length > 0 ) { this.#renderMap(); } return this; } #redrawSelection = () => { if ((this.widgetValues !== undefined) && (this.widgetValues.length > 0)) { let options = { color: "#3388ff", weight: 3, opacity: 1, lineCap: 'round', lineJoin: "round" , fillColor: "#3388ff", fillOpacity: 0.2, fillRule: "evenodd" } switch ((this.widgetValues[0].value.type as string)) { case 'Rectangle': console.log(this.widgetValues[0].value.coordinates) ; let bounds = L.latLngBounds(this.widgetValues[0].value.coordinates[0][0],this.widgetValues[0].value.coordinates[0][2]) ; L.rectangle(bounds, (options as PolylineOptions)).addTo(this.map); break; default: console.log(this.widgetValues[0].value.coordinates) ; let coordinates = this.widgetValues[0].value.coordinates[0][0] ; L.polygon(this.widgetValues[0].value.coordinates[0], (options as PolylineOptions)).addTo(this.map); break; } } } #renderMap = () => { let d = new Date(); let elementId = d.valueOf(); this.html.append($('<div id="map-'+elementId+'" class="map-wrapper"></div>')); this.map = L.map("map-"+elementId); let tl:TileLayer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap", }); tl.addTo(this.map); // attempt to rebind geoman - does not work at the moment // see https://geoman.io/docs/lazy-loading // L.PM.reInitLayer(tl); this.map.setView([this.configuration.center.lat, this.configuration.center.long], this.configuration.zoom); this.map.pm.addControls({ position: "topleft", cutPolygon: false, drawCircle: false, drawPolyline: false, drawCircleMarker: false, drawMarker: false, drawText: false, drawPolygon: true, editMode: true, dragMode: true, rotateMode: false, removalMode: true }); this.#redrawSelection() ; if ((this.widgetValues !== undefined) && (this.widgetValues.length > 0)) { var layers = []; //layers = L.PM.Utils.findLayers(this.map) //console.log(layers) ; //this.drawingLayer = layers[0] as Layer; this.map.pm.getGeomanLayers(false).forEach(layer => { if(layer instanceof L.Polygon) { layer.pm.enable() ; this.drawingLayer = layer ; } }); this.drawingLayer.on("pm:edit", (e) => { console.log('fireing pm:create') console.log(e); //let widgetValue = this.#setWidgetValue(e.layer) ; this.drawingLayer = e.layer; }); this.drawingLayer.on("pm:update", (e) => { console.log('fireing pm:update') console.log(e); //let widgetValue = this.#setWidgetValue(e.layer) ; this.drawingLayer = e.layer; }); } let submitMapOptions: CustomControlOptions = { name: "submitMap", block: "custom", title: I18n.labels.MapWidgetValidate, className: "submitMap icon-map-validate", onClick: () => { //this.widgetValue = [this.widgetValue] this.#setWidgetValue(this.drawingLayer) ; this.triggerRenderWidgetVal(this.widgetValues); $(this.parentComponent).trigger("change"); console.log(this) ; console.log(this.endClassWidgetGroup) ; //this.html.hide() ; //this.endClassWidgetGroup }, } this.map.pm.Toolbar.createCustomControl(submitMapOptions); this.map.on("pm:create", (e:any) => { console.log('fireing pm:create') //If there is already a drawing, then delete it // allows only for one drawing at a time if (this.drawingLayer) this.map.removeLayer(this.drawingLayer); this.drawingLayer = e.layer; this.map.addLayer(this.drawingLayer); //let widgetValue = this.#setWidgetValue(e.layer) ; console.log(this.endClassWidgetGroup) ; //this.endClassWidgetGroup.html[0].addEventListener("click", (evt:MouseEvent) => this.showWidgetMap(evt)) ; //this.renderWidgetVal(widgetValue); //add listener when the shape gets changed this.drawingLayer.on("pm:edit", (e) => { console.log('fireing pm:create pm:edit'); this.drawingLayer = e.layer; //let widgetValue = this.#setWidgetValue(e.layer) ; //this.#setWidgetValue(e.layer) ; //this.renderWidgetVal(widgetValue); }); }); this.map.on("pm:update", (e) => { console.log('fireing pm:update'); this.drawingLayer = e.layer; //let widgetValue = this.#setWidgetValue(e.layer) ; //this.#setWidgetValue(e.layer) ; //this.renderWidgetVal(widgetValue); //this.endClassWidgetGroup.html[0].addEventListener("click", (evt:MouseEvent) => this.showWidgetMap(evt)) ; }); /*this.map.on("pm:drawend", (e) => { console.log(e); this.drawingLayer = e.layer; });*/ this.#changeButton(); }; /*showWidgetMap(this: HTMLElement, ev: Event) { console.log(ev) ; }*/ /*private showWidgetMap(e: MouseEvent): void { let objectVal = this.endClassWidgetGroup.ParentComponent.ObjectPropertyGroup.objectPropVal ; this.endClassWidgetGroup.ParentComponent.EndClassGroup.editComponents = false ; this.endClassWidgetGroup.ParentComponent.EndClassGroup.onObjectPropertyGroupSelected(objectVal) ; this.endClassWidgetGroup.ParentComponent.endClassWidgetGroup.render() ; console.log(this) ; let _this = this.endClassWidgetGroup.ParentComponent.EndClassGroup.editComponents.widgetWrapper.widgetComponent ; //this.html = this.endClassWidgetGroup.ParentComponent.EndClassGroup.editComponents.widgetWrapper.widgetComponent.html ; //this.parentComponent = this.endClassWidgetGroup.ParentComponent.EndClassGroup.editComponents.widgetWrapper.widgetComponent.parentComponent ; //this.endClassWidgetGroup = this.parentComponent.ParentComponent.ParentComponent.ParentComponent.endClassWidgetGroup ; _this.widgetValue = this.widgetValue ; _this.#renderMap(true) ; //this.render() ; console.info(`After click event, 'this' refers to canvas and not to the instance of Foo:`); console.info(this); console.info(_this); console.warn(`Message is: "${_this.widgetValue}"`); // error }*/ #getValueLabel(layer: any) { let area = this.#polygonArea((layer as any).getLatLngs() as LatLng[][]) ; let coordinates = (layer as any).getLatLngs() as LatLng[][] ; return this.#getSvgSelection(coordinates) + '<span>' + area +' km²</span>' ; } #setWidgetValue = (layer:any) => { this.widgetValues = [] ; switch ((layer as any).pm._shape) { case 'Rectangle': this.widgetValues.push(new MapValue({ label: this.#getValueLabel(layer as Rectangle), type: 'Rectangle', coordinates: (layer as Rectangle).getLatLngs() as LatLng[][], })) break; default: this.widgetValues.push(new MapValue({ label: this.#getValueLabel(layer as Polygon), type: 'Polygon', coordinates: (layer as Polygon).getLatLngs() as LatLng[][], })); break; } return this.widgetValues ; } #closeMap = () => { if(this.widgetValues?.length > 0 ) { this.triggerRenderWidgetVal(this.widgetValues); $(this.parentComponent).trigger("change"); } }; parseInput(input:ObjectMapWidgetValue["value"]): MapValue { const parsedCoords = input.coordinates.map((c)=>{ return c.map((latlng)=>{ if(!("lat" in latlng) || !('lng' in LatLng) || isNaN(latlng.lat) || isNaN(latlng.lng)) return new LatLng(latlng.lat,latlng.lng) }) }) if(parsedCoords.length === 0) throw Error(`Parsing of ${input.coordinates} failed`) return new MapValue({ label: input.label, coordinates: parsedCoords, type: input.type } ); } #changeButton() { this.renderMapValueBtn.html.remove(); this.renderMapValueBtn = new AddUserInputBtn( this, I18n.labels.MapWidgetCloseMap, this.#closeMap ).render(); } #getSvgSelection(coordinates: LatLng[][]) { let bounds = L.latLngBounds(coordinates[0]) ; let startLeft = (bounds.getWest() as number); let startBottom = (bounds.getSouth() as number); let width = ((bounds.getEast() as number) - (bounds.getWest() as number)); let height = ((bounds.getNorth() as number) - (bounds.getSouth() as number)); let svgCoordinates = ''; let lat = 0; let lon = 0; coordinates[0].forEach((point:any) => { console.log(point) ; lat = point.lat - startBottom ; lon = point.lng - startLeft ; if(!(svgCoordinates == '')) { svgCoordinates += ' '; } svgCoordinates += lon+','+lat ; }); let svg = `<svg id="svgelem" width="30" height="30" viewBox="0 0 `+width+` `+height+`" xmlns="http://www.w3.org/2000/svg" style=" transform: rotateX(180deg);" preserveAspectRatio="xMidYMid meet"> <g><polygon points="`+svgCoordinates+`" style="fill:#ffffff;" /></g></svg>` ; return svg ; } #polygonArea(coords: any) { let total = 0; let arrayCoords = new Array() ; arrayCoords[0] = new Array() ; let index = 0 ; coords[0].forEach((point:any) => { let newSet = [point.lat, point.lng]; arrayCoords[0][index] = newSet ; index++; }); if (arrayCoords && arrayCoords.length > 0) { total += Math.abs(this.#ringArea(arrayCoords[0])); for (let i = 1; i < arrayCoords.length; i++) { total -= Math.abs(this.#ringArea(arrayCoords[i])); } } return Math.round(total) ; } #ringArea(coords: number[][]): number { const coordsLength = coords.length; const earthRadius = 6371008.8/1000; //km² const FACTOR = (earthRadius * earthRadius) / 2; const PI_OVER_180 = Math.PI / 180; if (coordsLength <= 2) return 0; let total = 0; let i = 0; while (i < coordsLength) { const lower = coords[i]; const middle = coords[i + 1 === coordsLength ? 0 : i + 1]; const upper = coords[i + 2 >= coordsLength ? (i + 2) % coordsLength : i + 2]; const lowerX = lower[0] * PI_OVER_180; const middleY = middle[1] * PI_OVER_180; const upperX = upper[0] * PI_OVER_180; total += (upperX - lowerX) * Math.sin(middleY); i++; } return total * FACTOR; } }