Search code examples
javascriptangularamchartsammap

How to use zoomToMapObject when using lat and long for putting locations on map


I am working on amcharts map in angular. I am binding locations using on lat and long, I don't have countries id rather have location name. I want to zoom specific location using zoomToMapObject.

I have dropdown, when I select one country from it, I want to zoom that.

This is my object of locationData

{
    "dId": 28046,
    "lat": 33.8627681,
    "long": -117.8870831,
    "loc": "Fullerton  CA"
    "color":"red"
},
    {
    "dId": 25535,
    "lat": 31.5649775,
    "long": -110.2591639,
    "loc": "Sierra Vista  AZ"
     "color":"yellow"
},
    "dId": 31222,
    "lat": 33.4308114,
    "long": -70.7828727,
    "loc": "Pudahuel  Metropolitana"
     "color":"green"
},
{
    "dId": 23280,
    "lat": 36.1676717,
    "long": -94.1298177,
    "loc": "Springdale  AR"
    "color":"red"
},

When I select dropdown, In SelectCountry how can I get my code work. These all variables are returning undefined, hence zoomToMapObject not working

public mapCountry_selected(country)
{
   setTimeout( () => {  
     this.mapChart.zoomToGeoPoint({latitude:country.lat,longitude:country.long},17,true)
   }, 100);
}

This is my code

public locationMap()
{
    this.mapChart = am4core.create("productsMap", am4maps.MapChart);
    this.mapChart .geodata = am4geodata_worldLow;
    this.mapChart .projection = new am4maps.projections.Miller();
    this.polygonSeries = this.mapChart .series.push(new am4maps.MapPolygonSeries());
    this.polygonSeries.exclude = ["AQ"];
    this.polygonSeries.useGeodata = true;
    this.polygonSeries.calculateVisualCenter = true;

    let imageSeries = this.mapChart .series.push(new am4maps.MapImageSeries());
  
    var place = imageSeries.mapImages.template;
    place.nonScaling = true;
    place.propertyFields.latitude = "lat";
    place.propertyFields.longitude = "long";

    imageSeries.data=this.locationData;
    var circle = place.createChild(am4core.Circle);
    circle.propertyFields.fill = "color";  
  
    imageSeries.heatRules.push({
     "target": circle,
     "property": "radius",
     "min": 3,
     "max": 10,
     "dataField": "value",
    })
}

Edit1:

I am showing circle for every location("target": circle) with three colors green, yellow, red. Now after adding true point 2nd and 3rd solved(from my comment). I need highlight/popover location after selecting one from dropdown so user can directly will come know this location I have selected as there are many locations around each other.

Edit2:

Map after selecting location from dropdown

enter image description here


Solution

  • Initial Response for Question Revision 1

    Recommended Approach

    Based on what information you have available it may be easier to consider using the zoomToGeoPoint method available on the MapChart object eg

    //assuming the selected country is as follows
    var country = { 
        "dId": 23280,
        "lat": 36.1676717,
        "long": -94.1298177,
        "loc": "Springdale  AR",
        "color":"red",
    };
    this.mapChart.zoomToGeoPoint({latitude:country.lat,longitude:country.long},17)
    

    where 17 is your desired zoom level. zoomToMapObject is especially useful when handling chart events.

    More specific to your question

    However, if you would like to use the zoomToMapObject method, Based on your example the following approach could be considered.

    For each data point in your dataset, and using the initial data stored in am4geodata_worldLow which has the "id" (eg. extracting the id for the first am4geodata_worldLow.features[0].id) for each polygon, you could determine for each country/place using the lat/lng coordinates which is closest to the center of each polygon.

    In the MapChart object you have stored in this.mapChart there are multiple series (vaguely considered as layers) which you would have to check for polygon or MapObjects. These can be extracted using the following:

    var mapPolygons = this.mapChart.series.values.flatMap(function(seriesItem){return seriesItem.mapPolygons.values;});
    

    You could then build a data structure mapping each of your custom country/places to the ids available in the map (for eg an object {"your-country-id":mapPolygonObject})

    For each mapPolygon you can extract coordinate points using

    var coordinates = mapPolygon.points[0][0]; //returns an array of x,y coordinates
    //eg
    coordinates = 
    [{"x":691.2713756097037,"y":137.68935508639663},{"x":691.2544487420046,"y":137.69150482342081},{"x":691.2544487420046,"y":137.67239542245633},{"x":691.2695833531237,"y":137.6702455263596}];
    

    You could then find the center/centroid of this polygon using these points as bounds and then for each of your country points you would find the euclidean distance between the polygon center point and your country. The shortest distance could assume that this country is associated with this point. Considerations may be made for distance (NB. Since you are working with coordinates, be sure to consider to 5 decimal points at least) as 50,000 miles (please observe your coordinate system for ways to convert lat/lng distances to metres/miles) may mean these are different places.

    You could then extract the ids and build your map object for use later in your script. This may be time consuming and compute intensive and ideally would be better if you did this before had and stored the necessary references to be used later.

    Another Approach to search for ids

    Moreover, if you had the country ids, then this would be simpler search as denoted below:

    //assuming the selected country is as follows
    var country = { 
        "dId": 23280,
        "lat": 36.1676717,
        "long": -94.1298177,
        "loc": "Springdale  AR",
        "color":"red",
    };
    //for this example I am extracting the country id from the object,it would be ideal to have it otherwise
    //NB. country ids are also available in the `am4geodata_worldLow` used to initialize the map
    var countryId = country.loc.split(" ")[1]; //this would return the country code - 'AR' 
    
    //we will now search the created chart for mapObjects with this id
    //this will give us an array of map objects/polygons with the id if found
    var possibleMapObjects = this.mapChart.series.values.map(function(seriesItem){
        return seriesItem.getPolygonById(countryId)
    }).flat();
    
    
    if(possibleMapObjects.length > 0){
       // we have found a map object
       var countryMapObject = possibleMapObjects[0]; //taking the first item of array
       //now we can zoom to map object
       //the 3rd parameter true implies centering the map
       this.mapChart.zoomToMapObject(countryMapObject,17,true)
    }
    

    Initial Response for Question Revision 2 (Edit1)

    Based on the updated code, I noticed a few issues, I was unable to see markers on your map so i made a few modifications. Also, the modifications now allow you to retrieve marker/map objects by id as I have augmented the data with the id you have used please see updated code below.

    I have created a dropdown/select node to simulate the onchange event, in the callback/event handler, onCountryChange you will see how we can retrieve a marker by it's new pseudo-id that we specified and call actions (I will open a popup) according to the Documented MapImage API

    locationData = [
    
      {
        "dId": 28046,
        "lat": 33.8627681,
        "long": -117.8870831,
        "loc": "Fullerton  CA",
        "color": "red"
      },
      {
        "dId": 25535,
        "lat": 31.5649775,
        "long": -110.2591639,
        "loc": "Sierra Vista  AZ",
        "color": "yellow"
      },
      {
        "dId": 31222,
        "lat": 33.4308114,
        "long": -70.7828727,
        "loc": "Pudahuel  Metropolitana",
        "color": "green"
      },
      {
        "dId": 23280,
        "lat": 36.1676717,
        "long": -94.1298177,
        "loc": "Springdale  AR",
        "color": "red"
      },
    ].map(function(place) {
      place.id = place.dId;
      return place;
    });
    
    this.mapChart = am4core.create("productsMap", am4maps.MapChart);
    this.mapChart.geodata = am4geodata_worldLow;
    this.mapChart.projection = new am4maps.projections.Miller();
    this.polygonSeries = this.mapChart.series.push(new am4maps.MapPolygonSeries());
    this.polygonSeries.exclude = ["AQ"];
    this.polygonSeries.useGeodata = true;
    this.polygonSeries.calculateVisualCenter = true;
    
    let imageSeries = this.mapChart.series.push(new am4maps.MapImageSeries());
    
    var place = imageSeries.mapImages.template;
    place.nonScaling = true;
    place.propertyFields.latitude = "lat";
    place.propertyFields.longitude = "long";
    
    imageSeries.data = locationData;
    var circle = place.createChild(am4core.Circle);
    circle.propertyFields.fill = "color";
    /// code below made circles visible and used value in {loc} / location name as tooltiptext
    circle.width = 20;
    circle.height = 20;
    circle.nonScaling = false;
    circle.tooltipText = "{loc}";
    circle.horizontalCenter = "middle";
    circle.verticalCenter = "bottom";
    
    var circle2 = imageSeries.mapImages.template.createChild(am4core.Circle);
    circle2.radius = 3;
    circle2.propertyFields.fill = "color";
    
    
    circle2.events.on("inited", function(event){
      animateBullet(event.target);
    })
    
    
    function animateBullet(circle) {
        var animation = circle.animate([{ property: "scale", from: 1, to: 5 }, 
     
       {property: "opacity", from: 1, to: 0 }], 1000, am4core.ease.circleOut);
        animation.events.on("animationended", function(event){
          animateBullet(event.target.object);
        })
    }
    
    imageSeries.heatRules.push({
      "target": circle,
      "property": "radius",
      "min": 3,
      "max": 10,
      "dataField": "value",
    });
    
    //just initializing the ui (you have already done this in angular)
    var ddlPlaceChooser = document.getElementById("ddlPlaceChooser");
    
    locationData.map(function(location) {
        var option = document.createElement("option");
        option.value = location.id;
        option.innerText = location.loc;
        return option;
      })
      .forEach(function(option) {
        ddlPlaceChooser.appendChild(option)
      })
    
    function onCountryChange(event) {
    
      //however you retrieve the selected id
      var placeId = ddlPlaceChooser.selectedOptions[0].value;
      //retrieve map object
      var mapObject = imageSeries.getImageById(placeId);
      //zoom to map object
      mapChart.zoomToMapObject(mapObject, 17, true);
      //optionally open popup with message of choice
      mapObject.openModal("You choose me!")
      //mapObject.openPopup("You choose me!")
    
    
    
    }
    
    ddlPlaceChooser.addEventListener('change', onCountryChange)
    <script src="https://www.amcharts.com/lib/4/core.js"></script>
    <script src="https://www.amcharts.com/lib/4/maps.js"></script>
    <script src="https://www.amcharts.com/lib/4/geodata/worldLow.js"></script>
    <script src="https://cdn.amcharts.com/lib/4/themes/animated.js"></script>
    <div>
      <label for="placeChooser">Choose Place</label>
      <select id="ddlPlaceChooser" name="placeChooser"></select>
    </div>
    <hr />
    <div id="productsMap"></div>