nickcam / FlareClusterLayer

ArcGIS javascript custom graphics layer. Create clusters...with flares.
https://flareclusterlayer.azurewebsites.net/index_v4.html
MIT License
134 stars 51 forks source link

Integrating with react-arcgis. #33

Closed ghost closed 6 years ago

ghost commented 6 years ago

Hey Nick!

Thank you so much for creating this library. I'm having bit of a trouble integrating it into an application i am working on. The project uses (React-arcgis NPM library)[https://www.npmjs.com/package/react-arcgis] and we're trying to implement clustering on the map. I am able to initialize the FeatureClusterLayer object but when i add it to the map, it provides me with an error saying 'the object added is neither a layer nor a promise that resolves to a layer.'

The WIP branch is here: https://github.com/bcgov/mds/tree/feature/clustering/frontend

I had to include the library as just a github import since it's not published as a npm module: https://github.com/bcgov/mds/tree/feature/clustering/frontend/src/FlareClusterLayer

I updated the FlareClusterLayer_v4.js so that I can import the FlareClusterLayer object. I am using the import in this file: https://github.com/bcgov/mds/blob/feature/clustering/frontend/src/components/maps/MinePin.js

I know you may not be familiar with the stack or the issue, but i'm mostly here to brainstorm or in case you may have run into this in past? Let me know.

Thanks

Details: Languages - React.js Arcgis 4.8

nickcam commented 6 years ago

Hi @FreshGyan, sorry I'm not familiar with react, but it seems like how you're including it is the problem.

I can think of two things to try. 1) Try importing fcl as though it is an arcgis component so the FlareClusterLayer_v4.js file is loaded using the same mechanism as arcgis. It seems that react-arcgis uses esri-loader internally, so have a look here to see how to load up external depedencies using esri-loader. https://github.com/Esri/esri-loader. (see the Configuring Dojo section).

2) Copy the typescript code into your project and compile it as part of your project. This may be tricky as it requires imports to arcgis components to compile, so may require some refactoring of those imports to work...I'm not sure of what form that may take though.

Hope it helps! Nick

ghost commented 6 years ago

Hey @nickcam . Excellent advice! Now i am using esri-loader to load the FlayerClusterLayer file. Currently to test I am just using the URL for hosted file on your server for the demo of the arcgis 4.x example. I don't think my dataset is what the layer expects though. I am able to instantiate the object and add it to the map, but nothing gets displayed on the map.

Here's the code i'm using to load (Very much just following your e.g. to get it working in react first)

loadModules(["esri/symbols/SimpleMarkerSymbol",
    "esri/renderers/ClassBreaksRenderer",
    "esri/symbols/SimpleLineSymbol",
    "esri/symbols/SimpleFillSymbol",
    "esri/geometry/SpatialReference",
    "esri/PopupTemplate",
    "http://flareclusterlayer.azurewebsites.net/fcl/FlareClusterLayer_v4.js"
    ]).then(([SimpleMarkerSymbol,
      ClassBreaksRenderer, SimpleLineSymbol, SimpleFillSymbol,
    SpatialReference, PopupTemplate, FlareClusterLayer]) => {
      var defaultSym = new SimpleMarkerSymbol({
        size: 6,
        color: "#FF0000",
        outline: null
    });

    var renderer = new ClassBreaksRenderer({
        defaultSymbol: defaultSym
    });
    renderer.field = "clusterCount";

    var smSymbol = new SimpleMarkerSymbol({ size: 22, outline: new SimpleLineSymbol({ color: [221, 159, 34, 0.8] }), color: [255, 204, 102, 0.8] });
    var mdSymbol = new SimpleMarkerSymbol({ size: 24, outline: new SimpleLineSymbol({ color: [82, 163, 204, 0.8] }), color: [102, 204, 255, 0.8] });
    var lgSymbol = new SimpleMarkerSymbol({ size: 28, outline: new SimpleLineSymbol({ color: [41, 163, 41, 0.8] }), color: [51, 204, 51, 0.8] });
    var xlSymbol = new SimpleMarkerSymbol({ size: 32, outline: new SimpleLineSymbol({ color: [200, 52, 59, 0.8] }), color: [250, 65, 74, 0.8] });

    renderer.addClassBreakInfo(0, 19, smSymbol);
    renderer.addClassBreakInfo(20, 150, mdSymbol);
    renderer.addClassBreakInfo(151, 1000, lgSymbol);
    renderer.addClassBreakInfo(1001, Infinity, xlSymbol);

    var areaRenderer;

    // if area display mode is set. Create a renderer to display cluster areas. Use SimpleFillSymbols as the areas are polygons
    var defaultAreaSym = new SimpleFillSymbol({
        style: "solid",
        color: [0, 0, 0, 0.2],
        outline: new SimpleLineSymbol({ color: [0, 0, 0, 0.3] })
    });

    areaRenderer = new ClassBreaksRenderer({
        defaultSymbol: defaultAreaSym
    });
    areaRenderer.field = "clusterCount";

    var smAreaSymbol = new SimpleFillSymbol({ color: [255, 204, 102, 0.4], outline: new SimpleLineSymbol({ color: [221, 159, 34, 0.8], style: "dash" }) });
    var mdAreaSymbol = new SimpleFillSymbol({ color: [102, 204, 255, 0.4], outline: new SimpleLineSymbol({ color: [82, 163, 204, 0.8], style: "dash" }) });
    var lgAreaSymbol = new SimpleFillSymbol({ color: [51, 204, 51, 0.4], outline: new SimpleLineSymbol({ color: [41, 163, 41, 0.8], style: "dash" }) });
    var xlAreaSymbol = new SimpleFillSymbol({ color: [250, 65, 74, 0.4], outline: new SimpleLineSymbol({ color: [200, 52, 59, 0.8], style: "dash" }) });

    areaRenderer.addClassBreakInfo(0, 19, smAreaSymbol);
    areaRenderer.addClassBreakInfo(20, 150, mdAreaSymbol);
    areaRenderer.addClassBreakInfo(151, 1000, lgAreaSymbol);
    areaRenderer.addClassBreakInfo(1001, Infinity, xlAreaSymbol);

    // Set up another class breaks renderer to style the flares individually
    var flareRenderer = new ClassBreaksRenderer({
        defaultSymbol: renderer.defaultSymbol
    });
    flareRenderer.field = "clusterCount";

    var smFlareSymbol = new SimpleMarkerSymbol({ size: 14, color: [255, 204, 102, 0.8], outline: new SimpleLineSymbol({ color: [221, 159, 34, 0.8] }) });
    var mdFlareSymbol = new SimpleMarkerSymbol({ size: 14, color: [102, 204, 255, 0.8], outline: new SimpleLineSymbol({ color: [82, 163, 204, 0.8] }) });
    var lgFlareSymbol = new SimpleMarkerSymbol({ size: 14, color: [51, 204, 51, 0.8], outline: new SimpleLineSymbol({ color: [41, 163, 41, 0.8] }) });
    var xlFlareSymbol = new SimpleMarkerSymbol({ size: 14, color: [250, 65, 74, 0.8], outline: new SimpleLineSymbol({ color: [200, 52, 59, 0.8] }) });

    flareRenderer.addClassBreakInfo(0, 19, smFlareSymbol);
    flareRenderer.addClassBreakInfo(20, 150, mdFlareSymbol);
    flareRenderer.addClassBreakInfo(151, 1000, lgFlareSymbol);
    flareRenderer.addClassBreakInfo(1001, Infinity, xlFlareSymbol);

      // set up a popup template
      var popupTemplate = new PopupTemplate({
        title: "{name}",
        content: [{
            type: "fields",
            fieldInfos: [
                { fieldName: "facilityType", label: "Facility Type", visible: true },
                { fieldName: "postcode", label: "Post Code", visible: true },
                { fieldName: "isOpen", label: "Opening Hours", visible: true }
            ]
        }]
    });

    var options = {
      id: "flare-cluster-layer",
      clusterRenderer: renderer,
      areaRenderer: areaRenderer,
      flareRenderer: flareRenderer,
      singlePopupTemplate: popupTemplate,
      displaySubTypeFlares: true,
      maxSingleFlareCount: 8,
      clusterRatio: 75,
      clusterAreaDisplay: "activated",
      data: data
    }

    const fcl = FlareClusterLayer.FlareClusterLayer(options);
    this.props.map.add(fcl);

I'm wondering if the way i have my data, is incorrect. Currently the data looks like this:

      const { id } = this.props.match.params;
      let mineIds = [];
      if (id) {
        mineIds = [id];
        this.setState({ isFullMap: false})
      }
      else {
        this.setState({ isFullMap: true})
        mineIds = this.props.mineIds;
      }
      const symbol = {
        "url": '../../../public/Pin.svg',
        "width": this.state.isFullMap ? '40' : '80',
        "height": this.state.isFullMap ? '40' : '80',
        "type": "picture-marker"
      };

      const graphicArray = mineIds.map((id) => {
        return (
          new Graphic({
            geometry: this.points(id),
            symbol: symbol,
            popupTemplate: this.state.isFullMap ? this.popupTemplate(id) : null
          })
        )
      });

So the data is just an array of Graphics. and each graphic has a geometry(Lat/Long) a symbol(Marker icon) and a custom popuptemplate.

Any suggestions on how to arrange the data in a way that the FCL constructor expects it to be?

nickcam commented 6 years ago

HI @FreshGyan - ok cool, you're almost there. Yeah the data format is wrong. You don't pass fcl an array of graphics, it will create those internally for you.

Just pass it an array of objects. Each object must have a property for x and y. By default fcl will look for properties called 'x' and'y' on your objects to use as the long and lat. But you can set the property name to look for by changing the xPropertyName and yPropertyName options of your fcl instance.

For example - pass it an array objects with this structure.

{
     x: number,
     y: number,

     // plus whatever other properties you want
     id: string,
     type: string, // etc, etc...

}

Or if your lat, long properties aren't called x and y you can do this

{
     long: number,
     lat: number,

     // plus whatever other properties you want
     id: string,
     type: string, // etc, etc...
}

var options = {
      id: "flare-cluster-layer",
      clusterRenderer: renderer,
      areaRenderer: areaRenderer,
      flareRenderer: flareRenderer,
      singlePopupTemplate: popupTemplate,
      displaySubTypeFlares: true,
      maxSingleFlareCount: 8,
      clusterRatio: 75,
      clusterAreaDisplay: "activated",
      data: data,
      xPropertyName: "long", // different long prop name
      yPropertyName: "lat" // different lat prop name
    }

    const fcl = FlareClusterLayer.FlareClusterLayer(options);

Also setting the symbol on the client data is irrelevant, the class breaks renderers you set on the fcl instance will determine symbology. Same with the popuptemplate....although after rendering you should be able to get to the layers graphics collection and edit the popuptemplate for the single points if you want to.

Hope that makes sense :).

ghost commented 6 years ago

AH okay! That makes sense πŸ€” I think i'm getting close.

So i have an array of objects (data) now, that look like this:

[
{y: "-124.8658685", x: "54.6853348", name: "0ca1fc1a-5975-4b61-b16b-88deb622c5e9"},
{y: "-121.5517847", x: "52.7423347", name: "5b2f3dcd-cd30-42ab-887e-fe3e63d9d881"}
]

and i'm instantiating my fcl and adding it to the map as explained in the example above. I also added the spatialreference same as in the index_v4.html to my options just in case.

Now no points are showing up on the map but when i zoom in/out of the map i get a console log of draw-data-2d: 1.669921875ms which means i am definitely hitting the draw function everytime i my map moves.

Maybe either the way I am adding it to the map is incorrect, or the way it renders. Any ideas?

Maybe it is getting rendered but isn't showing up on the map? I'm not sure what may cause that tho

nickcam commented 6 years ago

Hey man, the x and y should be numbers. Try changing your objects to this:

[
{y: -124.8658685, x: 54.6853348, name: "0ca1fc1a-5975-4b61-b16b-88deb622c5e9"},
{y: -121.5517847, x: 52.7423347, name: "5b2f3dcd-cd30-42ab-887e-fe3e63d9d881"}
]
ghost commented 6 years ago

Haha, that was my next hunch. unfortunately, it didn't make a difference :(

nickcam commented 6 years ago

Hmmm...ok. Are you able to create a plunkr or jsbin or some online demo just using vanilla javascript and the full set or subset of your data. Then post the link to it, can take a look at that.

ghost commented 6 years ago

Hey Nick! thank you for all your help. I got it working! The code looks a little messy but it seems to be working for now πŸ‘

My issue was not passing correct coordinates for x and y. I had them reversed πŸ€¦β€β™‚οΈ Also for CSS styling do you know what's the element name and how I can grab the different markers and circles? Or should I just rely on styling them during SimpleMarker/FillSymbol object initialization?

nickcam commented 6 years ago

Ok great, yeah I've done that before as well :)!

You should use the renderers and the symbols in them to do the styling. You can do heaps of stuff with renderers now, check out the arcgis js api pages.

In my example page I've got some custom styles to animate the appearance of the clusters, you could refer to those for getting at the cluster styles if needed. Certainly the individual markers should be done using standard arcgis symbols. 3d views use a canvas and not DOM elements so any styling applied won't work in that view.

matthieubouchard commented 5 years ago

Hey Nick! thank you for all your help. I got it working! The code looks a little messy but it seems to be working for now πŸ‘

My issue was not passing correct coordinates for x and y. I had them reversed πŸ€¦β€β™‚οΈ Also for CSS styling do you know what's the element name and how I can grab the different markers and circles? Or should I just rely on styling them during SimpleMarker/FillSymbol object initialization?

Thanks for asking all these questions! I have yet to implement it but have been going around in circles trying to use `setFeatureReduction({type: 'cluster', radius: 60}) on a FeatureLayer instance. I thought esri 3.x supported it. An yeah I'm sure you get the x & y thing now as latitude travels up the globe even though the lines are transverse and longitude side to side.