const layer = new this.GeoJSONLayer({
url: requestURL,
customParameters: {
request: this.state.bbox,
},
title: "New data",
renderer: {
type: "simple",
symbol: {
type: "simple-line",
width: 2,
Color: "blue"
}
}
});
JimuMapView.view.map.add(layer);
// After the layer is created try to build UI element from layer data.
layer.on('layerview-create', (event) => {
try{
const results = event.layerView.queryFeatures();
const graphics = results.features;
console.log(graphics); //Just returns null
}catch (error){
console.error("query failed: ", error);
}
})
There are two issues with your query.
Something like this example block.
view.on("pointer-move", function(event){
let query = featureLayer.createQuery();
query.geometry = view.toMap(event); // the point location of the pointer
query.distance = 2;
query.units = "miles";
query.spatialRelationship = "intersects"; // this is the default
query.returnGeometry = true;
query.outFields = [ "POPULATION" ];
featureLayerView.queryFeatures(query)
.then(function(response){
// returns a feature set with features containing the
// POPULATION attribute and each feature's geometry
});
});
Thanks for the feedback, appreciate it, however I'm still struggling to grasp scope and get your example plugged into my code.
Below is my widget.tsx which may not be written pretty but works for my testing.
In my custom widget when the button is pressed the selected map view extent is sent to an api server which returns GeoJSON that gets added to the map and the featuretable.
2 days now and I still can't fathom how to capture in the widget when the new layer is selected in the map that I can get the selected features data and show it in the lineDetailsTab.
I've tried plugging in your code but scope is proving confusing to me as I don't know where to get the featurelayerview from and the view.toMap scope is wrong as it always return undefined!!
I have also being trying to get this approach integrated:
Solved: Get Selected Features in Custom Widget - Esri Community
but I don't understand how to capture the datasource dynamically, as my geojson data source doesn't exist until the widget is run.
So 2 different directions being worked on but maybe I'm just grasping things wrong, when I add new geojson data to the mapview I want my widget to pick up when a feature is selected from that layer so could really do with another pointer on which approach is best to take?
import { React, FormattedMessage, defaultMessages as jimuCoreDefaultMessage, type AllWidgetProps, css, jsx, styled } from 'jimu-core';
import { type IMConfig } from '../config';
import { Tabs, Tab, Button } from 'jimu-ui';
import { loadArcGISJSAPIModules, type JimuMapView, JimuMapViewComponent } from 'jimu-arcgis';
import * as projection from "@arcgis/core/geometry/projection.js";
import SpatialReference from "@arcgis/core/geometry/SpatialReference.js";
import esriRequest from "@arcgis/core/request.js";
import FeatureTable from "@arcgis/core/widgets/FeatureTable.js";
import LayerView from "@arcgis/core/views/layers/LayerView.js";
import defaultMessages from './translations/default';
interface IState {
bbox: string
latitude: string
longitude: string
scale: number
zoom: number
mapViewReady: boolean
featureServiceUrlInput: string
jimuMapView: JimuMapView
linesLayer: geojson
}
projection.load();
export default class Widget extends React.PureComponent<AllWidgetProps<IMConfig>, IState> {
GeoJSONLayer: typeof __esri.GeoJSONLayer
SpatialReference: typeof __esri.SpatialReference
state = {
bbox: '',
latitude: '',
longitude: '',
zoom: 0,
scale: 0,
mapViewReady: false,
featureServiceUrlInput: '',
jimuMapView: null,
linesLayer: null
}
activeViewChangeHandler = (jmv: JimuMapView) => {
if (jmv){
this.setState({
jimuMapView: jmv
})
/* When the extent moves, update the state with all the updated values.*/
jmv.view.watch('extent', evt => {
const cs1 = new SpatialReference({
wkid: 4326 //WGS_84
});
/* EXTENT/BBOX conversion to Geometry Polygon:
BBOX(xMin,yMin,xMax,yMax)
POLYGON((xMin yMin,xMin yMax,xMax yMax,xMax yMin,xMin yMin))
*/
const extentClone = jmv.view.extent.clone();
const newExtent = projection.project(extentClone, cs1);
//console.log(JSON.stringify(newExtent.toJSON()));
const mapBBOX = "POLYGON((" + newExtent.xmin.toFixed(4) + " " + newExtent.ymin.toFixed(4) + "," + newExtent.xmin.toFixed(4) + " " + newExtent.ymax.toFixed(4) + "," + newExtent.xmax.toFixed(4) + " " + newExtent.ymax.toFixed(4) + "," + newExtent.xmax.toFixed(4) + " " + newExtent.ymin.toFixed(4) + "," + newExtent.xmin.toFixed(4) + " " + newExtent.ymin.toFixed(4) + "))";
this.setState({
bbox: mapBBOX,
latitude: jmv.view.center.latitude.toFixed(3),
longitude: jmv.view.center.longitude.toFixed(3),
scale: Math.round(jmv.view.scale * 1) / 1,
zoom: jmv.view.zoom,
mapViewReady: true
})
})
}
}
onLineListClick = (jmv: JimuMapView) => {
const linePopupTemplate = {
title: "Line details",
content:[{
type: "fields",
fieldInfos:[
{fieldName: "date_time", label: "Date:"},
{fieldName: "title", label: "Title:"},
{fieldName: "length", label: "Distance:"},
{fieldName: "user_name", label: "Owner:"},
{fieldName: "description", label: "Details:"},
]
}]
}
const requestURL = "https://api.example.com/route/api/"
// Lazy-loading (only load if/when needed) the ArcGIS API for JavaScript modules
// that we need - only once the "Get Lines" button is pressed.
loadArcGISJSAPIModules([
'esri/layers/GeoJSONLayer',
'esri/geometry/SpatialReference'
]).then((modules) => {
[this.GeoJSONLayer, this.SpatialReference] = modules;
if(this.state.linesLayer === null){
const layer = new this.GeoJSONLayer({
url: requestURL,
customParameters: {
auth: this.props.config.apiToken,
request: this.state.bbox,
type: 'geometry'
},
title: "API Lines",
renderer: {
type: "simple",
symbol: {
type: "simple-line",
width: 2,
color: "blue"
}
},
popupTemplate: linePopupTemplate
});
this.state.jimuMapView.view.map.add(layer);
this.setState({tracksLayer: layer}, () => {
//console.log("In Set state.");
//console.log(this.state);
});
// After the layer is created, create the feature table in the UI
layer.on('layerview-create', (event) => {
// Create the feature table
const featureTable = new FeatureTable({
view: this.state.jimuMapView.view, // Required for feature highlight to work
layer: layer,
visibleElements: {
// Autocast to VisibleElements
menuItems: {
clearSelection: true,
refreshData: false,
toggleColumns: true,
selectedRecordsShowAllToggle: true,
selectedRecordsShowSelectedToggle: true,
zoomToSelection: true
}
},
tableTemplate: {
// Autocast to TableTemplate
columnTemplates: [
// Takes an array of FieldColumnTemplate and GroupColumnTemplate
{
// Autocast to FieldColumnTemplate.
type: "field",
fieldName: "title",
label: "Title",
direction: "asc"
},
{
type: "field",
fieldName: "date_time",
label: "Date"
},
{
type: "field",
fieldName: "description",
label: "Description"
}
]
},
container: document.getElementById("lineList")
});
})
}else{
this.state.tracksLayer.customParameters.request = this.state.bbox;
this.state.tracksLayer.refresh();
}
})
}
render () {
const StyledButton = styled.button`
color: white;
background-color: #1074B2;
transition: 0.15s ease-in all;
&:hover {
background-color: #579EC8;
}
`
return <div className="widget-demo jimu-widget" style={{ overflow: 'auto' }}>
<Tabs>
<Tab id="listTab" title="tab1">
<StyledButton onClick={this.onLineListClick}>Get Linework</StyledButton>
{this.props.hasOwnProperty('useMapWidgetIds') && this.props.useMapWidgetIds && this.props.useMapWidgetIds.length === 1 &&
(<JimuMapViewComponent useMapWidgetId={this.props.useMapWidgetIds?.[0]} onActiveViewChange={this.activeViewChangeHandler}/>)
}
<div id="lineList" style={{ width: '100%', height: '600px', overflow: 'hidden' }}>
{this.props.intl.formatMessage({ id: '_widgetLabel', defaultMessage: defaultMessages._widgetLabel })}
</div>
</Tab>
<Tab id="lineDetailsTab" title="tab2">
<div id="lineDetailsContainer" style={{ width: '100%', height: '600px', overflow: 'hidden' }}>
{lineDetails}
</div>
</Tab>
</Tabs>
</div>
}
}
I don't think this current code is salvageable. It is time to start over. I could follow your code much better and this whole process should be easier if you could rewrite it in function based React.
So if I understand your goal, you want the user to click a button. Then the current map extents would be captured and sent to an API which would send back a GeoJSON of data that you would then add to your map.
I don't know how to do all of that, but I am willing to try to help you through it.
Could you instead load the GeoJSON layer first maybe even include it in your webmap and then query the data in your extent area? This will likely be a much simpler task.
I think you are making this more complicated than it needs to be. Here is what a add/remove layer widget looks like in function based React. Maybe you can use it as a base for your widget.
import { React, AllWidgetProps } from 'jimu-core'
import { JimuMapViewComponent, JimuMapView } from 'jimu-arcgis'
import FeatureLayer from 'esri/layers/FeatureLayer'
const { useState } = React
const Widget = (props: AllWidgetProps<any>) => {
const [jimuMapView, setJimuMapView] = useState<JimuMapView>()
const item = new FeatureLayer({
url: 'URL1'
})
const [layer
const activeViewChangeHandler = (jmv: JimuMapView) => {
if (jmv) {
setJimuMapView(jmv)
}
}
const formSubmit = (evt) => {
evt.preventDefault()
jimuMapView.view.map.add(layer)
}
const formSubmit3 = (evt) => {
evt.preventDefault()
jimuMapView.view.map.remove(layer)
}
return (
<div className="widget-starter jimu-widget">
{
props.useMapWidgetIds &&
props.useMapWidgetIds.length === 1 && (
<JimuMapViewComponent
useMapWidgetId={props.useMapWidgetIds?.[0]}
onActiveViewChange={activeViewChangeHandler}
/>
)
}
<form onSubmit={formSubmit}>
<div>
<button>Add Layer</button>
</div>
</form>
<form onSubmit={formSubmit3}>
<div>
<button>Delete</button>
</div>
</form>
</div>
)
}
export default Widget
Thanks again here for your input, helpful pointers and offer. Agree the current code is a mess but generally learning my way into stuff is messy but once I grasp the structures it all gets cleaned up. So yep, your example of functional react is much nicer and helpful as a first step, so I'll revisit my code and build to this style today.
"So if I understand your goal, you want the user to click a button. Then the current map extents would be captured and sent to an API which would send back a GeoJSON of data that you would then add to your map." Yes, currently the above code does this fine, I just can't get an on-map click event to report back to the widget what line feature was clicked on.
However,I think your point on the bigger picture of my data flow is very relevant too, so maybe a solution on pre-defining the data source in the widget settings would work better. Maybe the API data source gets added, in the background, as the widget loads and doesn't have to wait on the button click. The button click just views the data and filters it based on extent as in requirements below.
At the minute I only require the user to set a 'Select Map Widget' onto which my widget should draw/interact.
General requirements though are:
Hopefully this helpful but as I say I'll rewrite functionally, post and see what you think.
Thanks again.
If the only piece you are missing is finding what the user clicked on, the mapView.hitTest() method will return a list of all the graphics at a single point.
https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html#hitTest
Something like this example should do what you are looking for.
// get screenpoint from view's click event
view.on("click", function(event){
// Search for all features only on included layers at the clicked location
view.hitTest(event, {include: featureLayer}).then(function(response){
// if features are returned from the featureLayer, do something with results
if (response.results.length){
// do something
console.log(response.results.length, "features returned");
}
})
});
Okay, still working on rewriting code to the functional pattern and get working what had been, quite close but not quite there...
The functional pattern is much clearer for me to grasp so thanks for the template.
Anyway couple of specific issues around scope again, below changes/additions to your template are working for my purposes currently but I'm having to set the bbox state to var instead of const to use the updated bbox outside the extent watch event.
var is not recommended I read but can't seem to find the correct pattern to update it and follow recommended forms...
const [ jimuMapView, setJimuMapView ] = useState<JimuMapView>();
var [ bbox, setBBOX ] = useState('');
const activeViewChangeHandler = (jmv: JimuMapView) => {
if (jmv) {
setJimuMapView(jmv)
/* When the extent moves, update the state with all the updated values.*/
jmv.view.watch('extent', evt => {
const cs1 = new SpatialReference({
wkid: 4326 //WGS_84
});
/* EXTENT/BBOX conversion to Geometry Polygon:
BBOX(xMin,yMin,xMax,yMax)
POLYGON((xMin yMin,xMin yMax,xMax yMax,xMax yMin,xMin yMin))
*/
const extentClone = jmv.view.extent.clone();
const newExtent = projection.project(extentClone, cs1);
const mapBBOX = "POLYGON((" + newExtent.xmin.toFixed(4) + " " + newExtent.ymin.toFixed(4) + "," + newExtent.xmin.toFixed(4) + " " + newExtent.ymax.toFixed(4) + "," + newExtent.xmax.toFixed(4) + " " + newExtent.ymax.toFixed(4) + "," + newExtent.xmax.toFixed(4) + " " + newExtent.ymin.toFixed(4) + "," + newExtent.xmin.toFixed(4) + " " + newExtent.ymin.toFixed(4) + "))";
setBBOX(mapBBOX);
})
}
}
Other bit is as follows where the add layer functions work fine but I get a new layer for every button click where the extent has been change, scope again maybe but when I apply a layer refresh it doesn't refresh.
You should see below I'm trying to remove and re-add the layer but not quite working either and figure that's just a hack anyway..
var [ lineLayerAdded, setLineLayerAdded ] = useState(false);
const formSubmit = (evt) => {
evt.preventDefault();
//If layer has already been added to map then just refresh it.
if(lineLayerAdded){
console.log("In layer refresh");
//Now update layer parames and get new data
layer.customParameters.request = bbox;
//This is not refreshing
layer.refresh();
//Other approach here was to try and remove
//then re-add layer.
//So trying to add layer again after new bbox request.
//New layer works but now have 2+ layers now!!
jimuMapView.view.map.remove(layer);
jimuMapView.view.map.add(layer);
}else{
console.log("In layer add");
jimuMapView.view.map.add(layer);
setLineLayerAdded(true);
}
}
So, that add/remove layer widget is code from a introduction to React I was writing. I was making a point in that section that if the reference to layer is not stored in state it will become lost on re-render. I think that is exactly why your layer will not delete.
Ahh, lots in that article to pour over and once probably won't do it for me either🙄
Anyway, got it working as I want but some changes as I originally thought
const [layer
was a typo, but trying to add it in is giving me an error on using the Widget in EB.
Seems something is trying to close off the brace and not sure where you are adding in the 'item' featureLayer to that structure.
***********************
Load module error. Error: Module parse failed: Identifier 'layer' has already been declared (60:11)
File was processed with these loaders:
* ./node_modules/ts-loader/index.js
* ./node_modules/thread-loader/dist/cjs.js
* ./webpack/update-webpack-public-path-loader.js
You may need an additional loader to handle the result of these loaders.
| popupTemplate: videoPopupTemplate
| });
> const [layer];
| const activeViewChangeHandler = (jmv) => {
| if (jmv) {
**************************
So I adapted to the other approach I was using and the following is working fine now to add layer on first click, then only update/refresh layer with new data on an extent change and subsequent new click.
const [layer, setLayer ] = useState<GeoJSONLayer>();
const lineLayer = new GeoJSONLayer({
url: requestURL,
customParameters: {
request: bbox
},
title: "API Lines",
renderer: {
type: "simple",
symbol: {
type: "simple-line",
width: 2,
color: "blue"
}
},
popupTemplate: linePopupTemplate
});
const formSubmit = (evt) => {
evt.preventDefault();
//If layer has already been added to map then just refresh it.
if(lineLayerAdded){
//Now get new data
layer.customParameters.request = bbox;
layer.refresh();
}else{
jimuMapView.view.map.add(lineLayer);
setLineLayerAdded(true);
setLayer(lineLayer);
}
}
Just added in your get feature clicked example and it works perfect, so lots of struggling to grasp react sorted so thank you @JeffreyThompson2. Onto the next bit, sort through clicked features returned and build some UI elements from it...
jmv.view.on("click", function(event){
// Search for all features only on included layers at the clicked location
jmv.view.hitTest(event, {include: layer}).then(function(response){
// if features are returned from the featureLayer, do something with results
if (response.results.length){
// do something
console.log(response.results.length, "features returned");
}
})
});