AngularJS + ArcGIS
Asked Answered
R

2

9

I am trying to use ArcGIS JavaScript API inside an Angular application. As I see, it uses Dojo. So, I am trying to initialize ArcGIS from Angular directive like this:

  link: function (scope, element, attrs) {
    dojo.require('esri.map');
    var init = function () {
      console.log('dojo is ready');
      var map = new esri.Map("map-container", {
        center: [-111.3797, 56.7266 ],
        zoom: 16,
        basemap: "streets"
      });
      map.enableScrollWheelZoom()

    };
    dojo.addOnLoad(init);
  }

Looks like this way is not 100% correct because when I try to zoom by scrolling mouse wheel, I get this error:

Uncaught TypeError: Cannot call method 'apply' of null 

My question, is how to properly to inject ArcGIS functionality inside an Angular app?

Rode answered 9/7, 2013 at 11:44 Comment(0)
F
13

I think a very 'AngularJS' style approach to this would be something like the following. (fiddle here http://jsfiddle.net/technicolorenvy/2Ke62/4/)

I like using angular-ui-router, but this approach would also work w/ Angular's $routeProvider. The magic here is in the resolve object which will optionally 'wait' until a promise is resolved before continuing.

angular.module('webApp', ['ui.router'])
    // module (app) config
    .config(function ($stateProvider, $urlRouterProvider) {

        $urlRouterProvider.otherwise('/map');

        $stateProvider.state('map', {
            url: '/map',
            template: '<div id="map"></div>',
            controller: 'MapCtrl',
            resolve: {
                promiseObj: function ($q, $rootScope, wish) {
                    var deferred = $q.defer(),
                        deps = {
                            Map: 'esri/map',
                            FeatureLayer: 'esri/layers/FeatureLayer',
                            InfoTemplate: 'esri/InfoTemplate',
                            SimpleFillSymbol: 'esri/symbols/SimpleFillSymbol',
                            SimpleRenderer: 'esri/renderers/SimpleRenderer',
                            SimpleMarkerSymbol: 'esri/symbols/SimpleMarkerSymbol',
                            ScaleDependentRenderer: 'esri/renderers/ScaleDependentRenderer',
                            Color: 'dojo/_base/Color'
                        };

                    wish.loadDependencies(deps, function () {
                        deferred.resolve();
                        if (!$rootScope.$$phase) {
                            $rootScope.$apply();
                        }
                    });

                    return deferred.promise;
                }
            }
        });
    });

As you can see above, we have a map state that has a resolve prop. You can then build an object that represents your ArcGIS/Dojo dependencies and pass that on to your wish.loadDependencies (see below).

Using q we will return a promise that gets resolved once all the dependencies are loaded via dojo's require

angular.module('webApp')
     // service that deals w/ our dojo require
    .service('wish', function () {

        // it's not require... it's a wish?
        var wish = {};

        function _loadDependencies(deps, next) {
            var reqArr = _.values(deps),
                keysArr = _.keys(deps);

            // use the dojo require (required by arcgis + dojo) && save refs
            // to required obs
            require(reqArr, function () {
                var args = arguments;

                _.each(keysArr, function (name, idx) {
                    wish[name] = args[idx];
                });

                next();
            });
        }

        return {
            loadDependencies: function (deps, next) {
                _loadDependencies(deps, next);
            },

            get: function () {
                return wish;
            }
        };
    });

after that, in your MapCtrl, you can call all the ArcGIS/Dojo fns directly (as you would normally), by the keys you used in the deps object that was constructed during app config, as they are now attached to the object returned by wish.get().

What follows is a modified version of the example found here (https://developers.arcgis.com/en/javascript/jssamples/renderer_proportional_scale_dependent.html)

angular.module('webApp')
    // our map controller
    .controller('MapCtrl', function ($rootScope, $scope, wish) {

        var w = wish.get(),
            greenFill = new w.Color([133, 197, 133, 0.75]),
            greenOutline = new w.Color([133, 197, 133, 0.25]),
            layer,
            markerSym,
            renderer1,
            renderer2,

            CROPS_URL = 'http://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/USA_County_Crops_2007/FeatureServer/0';

        $scope.map = new w.Map('map', {
            center: [-98.579, 39.828],
            zoom: 4,
            basemap: 'gray'
        });

        layer = new w.FeatureLayer(CROPS_URL, {
            outFields: ['STATE', 'COUNTY', 'M086_07', 'AREA'],
            infoTemplate: new w.InfoTemplate('${COUNTY}, ${STATE}', '<div style="font: 18px Segoe UI">The percentage of the area of the county that represents farmland is <b>${M086_07}%</b>.</div>')
        });
        layer.setDefinitionExpression('AREA>0.01 and M086_07>0');


        markerSym = new w.SimpleMarkerSymbol();
        markerSym.setColor(greenFill);
        markerSym.setOutline(markerSym.outline.setColor(greenOutline));
        renderer1 = new w.SimpleRenderer(markerSym);
        renderer1.setProportionalSymbolInfo({
            field: 'M086_07',
            minSize: 1,
            maxSize: 10,
            minDataValue: 0,
            maxDataValue: 100
        });

        //for the second renderer increase the dot sizes and set a backgroundFillSymbol
        renderer2 = new w.SimpleRenderer(markerSym);
        renderer2.setProportionalSymbolInfo({
            field: 'M086_07',
            minSize: 5,
            maxSize: 15,
            minDataValue: 0,
            maxDataValue: 100
        });

        layer.setRenderer(new w.ScaleDependentRenderer({
            rendererInfos: [{
                'renderer': renderer1,
                    'minScale': 50000000,
                    'maxScale': 10000000
            }, {
                'renderer': renderer2,
                    'minScale': 0,
                    'maxScale': 5000000
            }]
        }));

        $scope.map.addLayer(layer);
    });

working fiddle demonstrating the above code, found here http://jsfiddle.net/technicolorenvy/2Ke62/4/

Fike answered 14/1, 2014 at 0:22 Comment(2)
I like your solution, but I have a small problem. I am using angular translate and it takes a split second to load init.js from "//js.arcgis.com/3.14" causing angular translate to show up as {{'something'|translate}} instead of the rendered text until init.js loads in order to load 'require'. Do you know of a way remove the require dependency inside the wish service ?Mantling
I'd handle this in your UI via preloader or 'data loaded' flags. a pretty decent explanation of the problem and possible solution here code-hound.com/add-a-preloader-to-your-website-using-angularjsFike
A
7

It looks like you are trying to build your map inside a directive element. That's a legitimate use, but I would make sure that you utilize the AMD loader of Dojo to load your modules and then bootstrap your Angular app after all the Dojo goodness is ready.

I recently put together a write-up on Angular/Esri dev, and the source code for a sample project can be found here.

What I actually do is build the map from a Controller, but the process should be similar to building it in the directive.

define([
    'angular',
    'esri/map'
], function(angular, Map) {
    function mapConfigs() {
        return {
            basemap: 'streets',
            center: [-118.1704035141802,34.03597014510993],
            zoom: 15
        };
    }
    function mapGen(elem) {
        return new Map(elem, mapConfigs());
    }
    function AppController($scope) {
        $scope.map = mapGen('map');
    }
    function init(App) {
        App.controller('AppCtrl', ['$scope', AppController]);
        return AppController;
    }
    return { start: init };
});

I use a bootstrap module to build all my Angular bits using the Esri/Dojo bits before bootstrapping the Angular application.

define([
    'angular',
    'controllers/AppController',
    'widgets/search/SearchBootstrap'
], function(angular, AppController, SearchBootstrap) {
    function init() {
        var App = angular.module('app', ['ui.bootstrap']);
        AppController.start(App);
        SearchBootstrap.start(App);
        // need to bootstrap angular since we wait for dojo/DOM to load
        angular.bootstrap(document.body, ['app']);
        return App;
    }
    return { start: init };
});

Hope that helps a bit.

Ardenia answered 24/7, 2013 at 14:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.