Criação de mapa usando o AmCharts 4

Visão Geral

Neste tópico será apresentado um exemplo geração de mapa utilizando a biblioteca amCharts 4. Nele será usado, no padrão do geoJSON, o mapa do Brasil

amCharts 4

Documentação: amCharts 4 Documentation

Adicionar HTML Element

Para fazer a criação do mapa é necessário criar um campo do tipo HTMLElement, para isso siga os seguintes passos:

  1. Clique com o botão direito no menu “Campos”

  2. Clique em “adicionar”

  3. No ComboBox selecione a opção “HTMLElement”

  4. No campo “Template Html” insira a seguinte tag html

<div id="map" style="width: 100%; height: 100%"></div>

Carregamento pelo banco de dados

Para efetuar o carregamento pelo banco de dados, iremos adicionar mais alguns componentes, sendo dois procedimentos e uma variavel.

No procedimento “LoadMap” iremos criar um record para popular a variavel “customProperties”. Para a geração desse record temos a tabela exemplo states

CREATE TABLE IF NOT EXISTS samples.custom_properties
(
    acronym text COLLATE pg_catalog."default" NOT NULL,
    population integer NOT NULL,
    gmt text COLLATE pg_catalog."default",
    dtinc date,
    CONSTRAINT states_pkey PRIMARY KEY (acronym)
)
Nessa tabela temos as informações de sigla e população que serão usadas no exemplo

Para popularmos a variavel iremos criar o record com as informações da tabela acima, siga o exemplo do código:

SELECT
'{
    "tooltipText": "{id} - População: {populacao}",
    "backgroundColor": "gainsboro",
    "hoverColor": "aqua",
    "labelProps": {
        "fontSize": 12,
        "color": "black"
    },
    "routes": [
        {
            "strokeWidth": 4,
            "color": "red",
            "target": {
                "radius": 5,
                "strokeWidth": 3,
                "fillColor": "blue",
                "color": "red",
                "position": {
                    "longitude": -45.000000,
                    "latitude": -18.000000
                }
            },
            "points": [
                {
                    "longitude": -50.000000,
                    "latitude": -20.000000
                },
                {
                    "longitude": -45.000000,
                    "latitude": -18.000000
                },
                {
                    "longitude": -42.000000,
                    "latitude": -15.000000
                },
                {
                    "longitude": -40.000000,
                    "latitude": -8.000000
                }
            ]
        }
    ],
    "properties":[
    '
        ||
        (
            SELECT
                string_agg(
                    '{ "id": "'
                    || acronym
                    || '", "labelText": "'
                    || acronym
                    || '", "populacao": '
                    || population
                    || ', "fill": "#00c735"'
                    || ' }' ,
                    ','
                )
            FROM samples.custom_properties
            where case 
                when 
                    '{?INPUT.DataInicial}' is not null and 
                    '{?INPUT.DataFinal}' is not null 
                then
                    dtinc between '{?INPUT.DataInicial}' and '{?INPUT.DataFinal}'
                else
                    true
                end
        )
        ||
    '
    ]
}' as field
Para modificar a cor de apenas um poligono. Inclua a propriedade "fiil"

No comando populate informe o campo do record

Arquivos JS para carregar mapa

Agora iremos criar o arquivos JS para carregar o mapa, neste passo a passo iremos demonstrar um arquivo JS que será gerado no carregamento da tela e no momento com informações fixas:

  1. Clique em “Arquivos Javascript”

  2. Clique em “Novo arquivo”

  3. Informe um nome e prossiga

  4. Adicione o código a seguir para gerar o mapa com o AmCharts

const myForm = (function () {

    // Adiciona Callbacks no Formulário
    latromi.formManager.setOnFormCreatedCallback(onFormCreated);
    latromi.formManager.setOnEventFiringCallback(onEventFiring);
    latromi.formManager.setOnFieldValueChangedCallback(onFieldValueChanged);

    // Ocorre logo após o form ser inicializado
    function onFormCreated(ev) { 
        latromiExtensions.loadScripts(['https://www.amcharts.com/lib/4/core.js'
                                  ,'https://www.amcharts.com/lib/4/maps.js'
                                  ], loadJson); 
    }

    // Ocorre quando um evento é disparado no Form
    function onEventFiring(ev) {
    }

    // Ocorre quando o valor de um campo é alterado no Form
    function onFieldValueChanged(ev) { }
    
    // ========Inicio funções auxiliares==========
    function getProperties() {
        const jsonData = latromi
                            .formManager
                            .getFormInstance()
                            .getVariableValue("customProperties");
        return JSON.parse(jsonData);
    }
    
    function findCustomProperties(geoJsonSetting, id) {
        return geoJsonSetting.properties.find((prop) => prop.id == id);
    }
    
    function standardizeColor(str){
        if (!str) return;
        const ctx = document.createElement("canvas").getContext("2d");
        ctx.fillStyle = str;
        return ctx.fillStyle;
    }
    // ========Final funções auxiliares==========
    
    // Captura dados de poligonos do mapa
    function loadJson() {
        fetch("https://cdn.amcharts.com/lib/4/geodata/json/brazilLow.json").
        then(function (data) {
            return data.json()
       })
        .then(function (json) {
            loadMap(json)
        })
    }
    
    // Carrega o mapa com suas modificações
    function loadMap (geoJsonData) {
        // amCharts4 examplo
        // Cria instancia do mapa
        const chart = am4core.create("map", am4maps.MapChart);
        
        // Captura propriedades customizadas
        const geoJsonSettings = getProperties();
        
        // Configura opções do mapa
        chart.geodata = geoJsonData;
    
        // Configura projeção
        chart.projection = new am4maps.projections.Miller();
    
        // Cria uma serie poligonal do mapa
        var polygonSeries = chart.series.push(new am4maps.MapPolygonSeries());
    
        // Faz o carregamento dos poligonos do mapa (Ex.: Nomes dos países) dados do GeoJSON
        polygonSeries.useGeodata = true;
        polygonSeries.calculateVisualCenter = true;
    
        // Configura as series 
        const polygonTemplate = polygonSeries.mapPolygons.template;
        polygonTemplate.tooltipText = geoJsonSettings.tooltipText;
        polygonTemplate.fill = am4core.color(standardizeColor(geoJsonSettings.backgroundColor));
        polygonTemplate.propertyFields.id = "id";
    
        // Modificar propriedades individualmente
        polygonSeries.data = geoJsonSettings.properties;
        polygonTemplate.propertyFields.fill = "fill";

      // Adiciona as linhas no mapa
      for (let index = 0; index < customProperties.routes.length; index++) {
        const route = customProperties.routes[index];
        // Adiciona a serie das linhas
        const lineSeries = chart.series.push(new am4maps.MapLineSeries());
        lineSeries.mapLines.template.strokeWidth = route.strokeWidth;
        lineSeries.mapLines.template.stroke = am4core.color(standardizeColor(route.color));
        lineSeries.mapLines.template.nonScalingStroke = true;
        
        const line = lineSeries.mapLines.create();
        line.multiGeoLine = [route.points];
    
        // Adiciona um objeto por posição no mapa
        const placeSeries = chart.series.push(new am4maps.MapImageSeries());
        const place = placeSeries.mapImages.template;
        place.nonScaling = true;
        place.propertyFields.latitude = "latitude";
        place.propertyFields.longitude = "longitude";
        placeSeries.data = [route.target.position];
        
        const circle = place.createChild(am4core.Circle);
        circle.radius = route.target.radius;
        circle.fill = am4core.color(standardizeColor(route.target.fillColor));
        circle.stroke = am4core.color(standardizeColor(route.target.color));
        circle.strokeWidth = route.target.strokeWidth;
      }
    
        // Configura labels nas series
        const labelSeries = chart.series.push(new am4maps.MapImageSeries());
        const labelTemplate = labelSeries.mapImages.template.createChild(am4core.Label);
        labelTemplate.horizontalCenter = "middle";
        labelTemplate.verticalCenter = "middle";
        labelTemplate.fontSize = geoJsonSettings.labelProps.fontSize;
        labelTemplate.stroke = am4core.color(standardizeColor(geoJsonSettings.labelProps.color))
        labelTemplate.interactionsEnabled = false;
        labelTemplate.nonScaling = false;
        
        const allIdList = [];
        for (let index = 0; index < geoJsonData.features.length; index++) {
            allIdList.push(geoJsonData.features[index].id);
        }
    
        polygonSeries.events.on("inited", function () {
            for (let index = 0; index < allIdList.length; index++) {
                const polygon = polygonSeries.getPolygonById(allIdList[index]);
                const label = labelSeries.mapImages.create();
                const properties = findCustomProperties(geoJsonSettings, allIdList[index]);
                
                if (!properties)
                    continue;
                
                label.latitude = polygon.visualLatitude;
                label.longitude = polygon.visualLongitude;
                label.children.getIndex(0).text = properties.labelText ?? "";
            }
        })
    
        // Cria uma estado de Hover e configura um preenchimento de cor alternativa
        var hs = polygonTemplate.states.create("hover");
        hs.properties.fill = am4core.color(standardizeColor(geoJsonSettings.hoverColor));
    }

    // Todas as funções acima são "privadas", e não podem ser chamadas externamente.
    // As únicas funções que podem ser chamadas externamente são as que compões o resultado a seguir:
    return {
        load: () => {
            latromiExtensions.loadScripts(['https://www.amcharts.com/lib/4/core.js'
                                  ,'https://www.amcharts.com/lib/4/maps.js'
                                  ], loadJson);
        }
    }
})();

Atualização de dados com filtro

Para complementar o exemplo iremos incluir um filtro por data inicial e final para atualizar o mapa. Para isso iremos criar 3 campos a mais e adicionar o evento de click para o botão

2 curtidas

Ótimo! Mas ficou uma dúvida, como popular o campo HTML Element com os dados que vierem diretamente do nosso banco? Por exemplo, um mapa que contabiliza o destino das coletas puxando isso direto do nosso banco via Query?

Opa, bom tarde @Diego_Alves.

Atualizei o topico adicionando as informações que exemplificam o preenchimento com informações da base de dados.

Obs.: Foi atualizado o código JS também para permitir copia-lo sem necessidade de alteração. Pois ele agora espera receber aquele corpo JSON sendo preenchido pela base de dados

1 curtida

Mapa funcional via BD Diego, boa!
Mas surgiu uma outra coisa… Em todos os nossos forms, usamos data inicial e final, via campos com o botão de filtro. Normalmente usamos o UpdateDataSource e funciona. Como este mapa é baseado em JS, como ficaria essa parte de atualizar o nosso mapa de acordo com a data escolhida?

1 curtida

Bom dia, @Diego_Alves.

Atualizei o exemplo incluindo o filtro por data

Obs.: Foi atualizado o script JS, SQL e a tabela usado no exemplo

1 curtida

Um dica extra para a manipulação de mapa usando o AmCharts 4 com o Latromi. Adicionando no código JS, ele irá capturar o click em um Estado e enviar o resultado do evento para o btnFiltrar

// Dispara evento clique no mapa
polygonTemplate.events.on("hit", function (ev) {
    const form = latromi.formManager.getFormInstance();
    form.raiseFieldEvent('btnFiltrar', 'Click', ev.target.dataItem.dataContext);
});
1 curtida

Para a Criação de mapas que ultilizam coordenadas na tela, por exemplo, varios caminhoes rodando no mapa, precisaria que o mapa me desse varios pontos no mapa indicando onde esta cada caminhão, como ficaria?

Boa tarde @Diego_Alves,

Há duas formas de incluir as linhas no mapa semelhante à situação informada, a primeira forma é com a ideia de progresso da rota, segue o exemplo:

  • Adicionar as seguintes keys no arquivo json:
"routes": [
    {
        "strokeWidth": 4,
        "color": "red",
        "target": {
            "radius": 5,
            "strokeWidth": 3,
            "fillColor": "blue",
            "color": "red",
            "progress": 55.75
        },
        "points": [
            {
                "longitude": -50.000000,
                "latitude": -20.000000
            },
            {
                "longitude": -45.000000,
                "latitude": -18.000000
            },
            {
                "longitude": -42.000000,
                "latitude": -15.000000
            },
            {
                "longitude": -40.000000,
                "latitude": -8.000000
            }
        ]
    }
]
  • No código JS adicionar próximo à parte que inclui as Labels:
// Adiciona as linhas no mapa
for (let index = 0; index < customProperties.routes.length; index++) {
    const route = customProperties.routes[index];
    // Adiciona a serie das linhas
    const lineSeries = chart.series.push(new am4maps.MapLineSeries());
    lineSeries.mapLines.template.strokeWidth = route.strokeWidth;
    lineSeries.mapLines.template.stroke = am4core.color(standardizeColor(route.color));
    lineSeries.mapLines.template.nonScalingStroke = true;
    
    const line = lineSeries.mapLines.create();
    line.multiGeoLine = [route.points];
  
    // Adiciona um objeto na linha do mapa
    const bullet = line.lineObjects.create();
    bullet.nonScaling = true;
    bullet.position = route.target.progress / 100; // define quantos por cento da rota foi completa
    
    const circle = bullet.createChild(am4core.Circle);
    circle.radius = route.target.radius;
    circle.strokeWidth = route.target.strokeWidth;
    circle.fill = am4core.color(standardizeColor(route.target.fillColor));
    circle.stroke = am4core.color(standardizeColor(route.target.color));
}

O Segundo exemplo seria com uma ideia de posição real no mapa, onde indicará a posição do objeto sem ficar vinculado obrigatoriamente com a linha:

  • Adicionar as seguintes keys no arquivo json:
"routes": [
    {
        "strokeWidth": 4,
        "color": "red",
        "target": {
            "radius": 5,
            "strokeWidth": 3,
            "fillColor": "blue",
            "color": "red",
            "position": {
                "longitude": -45.000000,
                "latitude": -18.000000
            }
        },
        "points": [
            {
                "longitude": -50.000000,
                "latitude": -20.000000
            },
            {
                "longitude": -45.000000,
                "latitude": -18.000000
            },
            {
                "longitude": -42.000000,
                "latitude": -15.000000
            },
            {
                "longitude": -40.000000,
                "latitude": -8.000000
            }
        ]
    }
]
  • No código JS adicionar próximo à parte que inclui as Labels:
// Adiciona as linhas no mapa
for (let index = 0; index < customProperties.routes.length; index++) {
    const route = customProperties.routes[index];
    // Adiciona a serie das linhas
    const lineSeries = chart.series.push(new am4maps.MapLineSeries());
    lineSeries.mapLines.template.strokeWidth = route.strokeWidth;
    lineSeries.mapLines.template.stroke = am4core.color(standardizeColor(route.color));
    lineSeries.mapLines.template.nonScalingStroke = true;
    
    const line = lineSeries.mapLines.create();
    line.multiGeoLine = [route.points];
  
    // Adiciona um objeto por posição no mapa
    const placeSeries = chart.series.push(new am4maps.MapImageSeries());
    const place = placeSeries.mapImages.template;
    place.nonScaling = true;
    place.propertyFields.latitude = "latitude";
    place.propertyFields.longitude = "longitude";
    placeSeries.data = [route.target.position];
    
    const circle = place.createChild(am4core.Circle);
    circle.radius = route.target.radius;
    circle.fill = am4core.color(standardizeColor(route.target.fillColor));
    circle.stroke = am4core.color(standardizeColor(route.target.color));
    circle.strokeWidth = route.target.strokeWidth;
}

Você também pode usar a solução nativa do Latromi para construir mapas usando o Google Maps.

Fica muito fácil! :heart_eyes:

Dá uma conferida na nossa documentação: Consulta:CrossMap - LATROMI Manuais