Esri / Viewer

Viewer is a configurable application template that enables you to display an ArcGIS web map using a variety of tools.
Apache License 2.0
60 stars 117 forks source link

Share Tool - current map extent #35

Closed tscharff closed 9 years ago

tscharff commented 9 years ago

In a scenario where a webmap contains a locally hosted (non ArcGIS.com) map service that is in a different projected coordinate system, the Basic Viewer Share tool does not update the URL (in Map Link or Embed Map) to include the map extent. (even though the box for "Share current map extent" is checked)

While troubleshooting the issue, I created a locally hosted map service that is in Web Mercator Auxiliary Sphere (and a corresponding webmap in ArcGIS.com). In this scenario the Share Tool generates the URL correctly, and allows the Basic Viewer to correctly zoom to the specified map extent when the map is shared.

jgravois commented 9 years ago

thanks for taking the time to report your findings. this is because map.geographicExtent (in the API itself) is only available for web mercator applications. the relevant code in the template is here

Its possible to enhance to add similar support for other coordinate systems, but it would require a fairly significant refactor and an external request to a external geometry service to retrieve lat/longs.

tscharff commented 9 years ago

Thank you for the quick reply.

Yeah, that was my guess, but there were 2 contributing factors that made it less clear: I couldn't find documentation about it for the Basic Viewer (and Classic Viewer); and the share function works as expected for the webmap, itself (inside the map viewer).

We are trying to deploy an application that will display capital projects throughout a service area. Our webmaster would like to launch the app from a list of projects... when the user clicks on a project, the app will open with the map automatically zoomed to the project area. I was originally trying to find a way to zoom to a feature using URL parameters, but was willing to use the extent as a workaround. Without the capability, the webmaster is proposing to use Google maps.

If URL parameters for zoom to extent represents a significant effort for other coordinate systems, perhaps the effort to implement URL parameters for zoom to feature would require much less effort (since the app is already capable of zooming to features via context menu and pop-ups, etc).

Below are a few links to related ArcGIS Ideas... it appears there is support from the user community to add related functionality.

ArcGIS Idea for consistent share function... https://c.na9.visual.force.com/apex/ideaView?id=087E00000005Aw7IAE

ArcGIS Ideas for URL parameters to zoom to feature... https://c.na9.visual.force.com/apex/ideaView?id=087E00000005IDR&returnUrl=%2Fapex%2FideaList%3Fc%3D09a300000004xET%26category%3DArcGIS%2BOnline

https://c.na9.visual.force.com/apex/ideaView?id=08730000000bsud&returnUrl=%2Fapex%2FideaList%3Fc%3D09a300000004xET%26category%3DArcGIS%2BOnline

jgravois commented 9 years ago

we appreciate the feedback @tscharff. that type of functionality could make its way into the Viewer in a future release, but the code itself is extensible and written using your JavaScript API so you are welcome to incorporate it in your own project immediately.

the code i referenced in my earlier reply demonstrates how to parse values from a url query so please feel free to use it as an example if you'd like to incorporate your own alternative custom logic.

kellyhutchins commented 9 years ago

@tscharff There may be some options for working around this issue I'd just like to understand a bit more about your map. Are you using the hosted template or have you downloaded the code and have it hosted on your own web server? Do you add your locally hosted service to the map after its created?

tscharff commented 9 years ago

We are using a hosted template.

I created a new webmap that uses a locally hosted (non ArcGIS.com) basemap, and that also contains our locally hosted service containing the capital projects. (Both services are in a state plane projection.) Afterward, I saved the map, and then created the web map application based on a template.

Hope that helps. If you need more info, please let me know. Thanks!

kellyhutchins commented 9 years ago

@tscharff I have an updated version of ShareDialog.js that I think will work for your situation. Can you test and let me know? If it does work for you I'll work on incorporating this logic (after more testing on my end) into the Viewer template for the next release.

define(["dojo/Evented", "dojo/_base/declare", "dojo/Deferred", "dojo/_base/lang", "dojo/has", "esri/kernel", "esri/config", "esri/SpatialReference", "dijit/_WidgetBase", "dijit/a11yclick", "dijit/_TemplatedMixin", "dojo/on",
// load template
"dojo/text!application/dijit/templates/ShareDialog.html", "dojo/i18n!application/nls/ShareDialog", "dojo/dom-class", "dojo/dom-style", "dojo/dom-attr", "dojo/dom-construct", "esri/request", "esri/urlUtils", "dijit/Dialog", "dojo/number", "dojo/_base/event"], function (
Evented, declare, Deferred, lang, has, esriNS, esriConfig, SpatialReference, _WidgetBase, a11yclick, _TemplatedMixin, on, dijitTemplate, i18n, domClass, domStyle, domAttr, domConstruct, esriRequest, urlUtils, Dialog, number, event) {
    var Widget = declare("esri.dijit.ShareDialog", [_WidgetBase, _TemplatedMixin, Evented], {
        templateString: dijitTemplate,
        options: {
            theme: "ShareDialog",
            dialog: null,
            useExtent: false,
            map: null,
            url: window.location.href,
            image: "",
            title: window.document.title,
            summary: "",
            hashtags: "",
            mailURL: "mailto:%20?subject={title}&body={summary}%20{url}",
            facebookURL: "https://www.facebook.com/sharer/sharer.php?s=100&p[url]={url}&p[images][0]={image}&p[title]={title}&p[summary]={summary}",
            twitterURL: "https://twitter.com/intent/tweet?url={url}&text={title}&hashtags={hashtags}",
            googlePlusURL: "https://plus.google.com/share?url={url}",
            bitlyAPI: location.protocol === "https:" ? "https://api-ssl.bitly.com/v3/shorten" : "http://api.bit.ly/v3/shorten",
            bitlyLogin: "",
            bitlyKey: "",
            embedSizes: [{
                "width": "100%",
                "height": "640px"
            },
            {
                "width": "100%",
                "height": "480px"
            },
            {
                "width": "100%",
                "height": "320px"
            },
            {
                "width": "800px",
                "height": "600px"
            },
            {
                "width": "640px",
                "height": "480px"
            },
            {
                "width": "480px",
                "height": "320px"
            }]
        },
        // lifecycle: 1
        constructor: function (options, srcRefNode) {
            // mix in settings and defaults
            var defaults = lang.mixin({}, this.options, options);
            // widget node
            this.domNode = srcRefNode;
            this._i18n = i18n;
            // properties
            this.set("theme", defaults.theme);
            this.set("url", defaults.url);
            this.set("visible", defaults.visible);
            this.set("dialog", defaults.dialog);
            this.set("embedSizes", defaults.embedSizes);
            this.set("embedHeight", defaults.embedHeight);
            this.set("embedWidth", defaults.embedWidth);
            this.set("mailURL", defaults.mailURL);
            this.set("facebookURL", defaults.facebookURL);
            this.set("twitterURL", defaults.twitterURL);
            this.set("googlePlusURL", defaults.googlePlusURL);
            this.set("bitlyAPI", defaults.bitlyAPI);
            this.set("bitlyLogin", defaults.bitlyLogin);
            this.set("bitlyKey", defaults.bitlyKey);
            this.set("image", defaults.image);
            this.set("title", defaults.title);
            this.set("summary", defaults.summary);
            this.set("hashtags", defaults.hashtags);
            this.set("useExtent", defaults.useExtent);
            // listeners
            this.watch("theme", this._updateThemeWatch);
            this.watch("url", this._updateUrl);
            this.watch("visible", this._visible);
            this.watch("embedSizes", this._setSizeOptions);
            this.watch("embed", this._updateEmbed);
            this.watch("bitlyUrl", this._updateBitlyUrl);
            this.watch("useExtent", this._useExtentChanged);
            // classes
            this.css = {
                container: "button-container",
                embed: "embed-page",
                button: "toggle-grey",
                buttonSelected: "toggle-grey-on",
                icon: "icon-share",
                linkIcon: "icon-link share-dialog-icon",
                facebookIcon: "icon-facebook-squared share-dialog-icon",
                twitterIcon: "icon-twitter share-dialog-icon",
                gplusIcon: "icon-gplus share-dialog-icon",
                emailIcon: "icon-mail-alt share-dialog-icon",
                mapSizeLabel: "map-size-label",
                shareMapURL: "share-map-url",
                iconContainer: "icon-container",
                embedMapSizeDropDown: "embed-map-size-dropdown",
                shareDialogContent: "dialog-content",
                shareDialogSubHeader: "share-dialog-subheader",
                shareDialogTextarea: "share-dialog-textarea",
                shareDialogExtent: "share-dialog-extent",
                shareDialogExtentChk: "share-dialog-checkbox",
                mapSizeContainer: "map-size-container",
                embedMapSizeClear: "embed-map-size-clear",
                iconClear: "icon-clear"
            };
        },
        // bind listener for button to action
        postCreate: function () {
            this.inherited(arguments);
            this._setExtentChecked();
            this._shareLink();
            this.own(on(this._extentInput, a11yclick, lang.hitch(this, this._useExtentUpdate)));
        },
        // start widget. called by user
        startup: function () {
            this._init();

        },
        // connections/subscriptions will be cleaned up during the destroy() lifecycle phase
        destroy: function () {
            this.inherited(arguments);
        },
        /* ---------------- */
        /* Public Events */
        /* ---------------- */
        // load
        /* ---------------- */
        /* Public Functions */
        /* ---------------- */

        /* ---------------- */
        /* Private Functions */
        /* ---------------- */
        _setExtentChecked: function () {
            domAttr.set(this._extentInput, "checked", this.get("useExtent"));
        },
        _useExtentUpdate: function () {
            var value = domAttr.get(this._extentInput, "checked");
            this.set("useExtent", value);
        },
        _useExtentChanged: function () {
            this._updateUrl();
            this._shareLink();
        },
        _setSizeOptions: function () {
            // clear select menu
            this._comboBoxNode.innerHTML = "";
            // if embed sizes exist
            if (this.get("embedSizes") && this.get("embedSizes").length) {
                // map sizes
                for (var i = 0; i < this.get("embedSizes").length; i++) {
                    if (i === 0) {
                        this.set("embedWidth", this.get("embedSizes")[i].width);
                        this.set("embedHeight", this.get("embedSizes")[i].height);
                    }
                    var option = domConstruct.create("option", {
                        value: i,
                        innerHTML: this.get("embedSizes")[i].width + " x " + this.get("embedSizes")[i].height
                    });
                    domConstruct.place(option, this._comboBoxNode, "last");
                }
            }
        },
        _updateUrl: function () {
            // nothing currently shortened
            this._shortened = null;
            // no bitly shortened
            this.set("bitlyUrl", null);
            // vars
            var map = this.get("map"),
                url = this.get("url"),
                useSeparator;
            // get url params
            var urlObject = urlUtils.urlToObject(window.location.href);
            urlObject.query = urlObject.query || {};
            urlObject.query.extent = null;
            // include extent in url
/* if (this.get("useExtent") && map) {
                    this._projectGeometry(map);
                }*/
            this._projectGeometry().then(lang.hitch(this, function (result) {
                if (result) {
                    var gExtent = result;
                    urlObject.query.extent = gExtent.xmin.toFixed(4) + ',' + gExtent.ymin.toFixed(4) + ',' + gExtent.xmax.toFixed(4) + ',' + gExtent.ymax.toFixed(4);
                }
                // create base url
                url = window.location.protocol + "//" + window.location.host + window.location.pathname;
                // each param
                for (var i in urlObject.query) {
                    if (urlObject.query[i]) {
                        // use separator 
                        if (useSeparator) {
                            url += "&";
                        } else {
                            url += "?";
                            useSeparator = true;
                        }
                        url += i + "=" + urlObject.query[i];
                    }
                }
                // update url
                this.set("url", url);
                // reset embed code
                this._setEmbedCode();
                // set url value
                domAttr.set(this._shareMapUrlText, "value", url);
                domAttr.set(this._linkButton, "href", url);
            }));

        },
        _projectGeometry: function () {
            var deferred = new Deferred();
            var map = this.get("map");
            if (this.get("useExtent") && map) {
                // get map extent in geographic
                if (map.geographicExtent) {
                    deferred.resolve(map.geographicExtent);
                    // var gExtent = map.geographicExtent;
                    // set extent string
                    //urlObject.query.extent = gExtent.xmin.toFixed(4) + ',' + gExtent.ymin.toFixed(4) + ',' + gExtent.xmax.toFixed(4) + ',' + gExtent.ymax.toFixed(4);
                } else {
                    //project the extent to geographic
                    var outSR = new SpatialReference({
                        "wkid": 4326
                    });
                    esriConfig.defaults.geometryService.project([map.extent], outSR).then(lang.hitch(this, function (result) {
                        if (result.length) {
                            var projectedExtent = result[0];
                            deferred.resolve(projectedExtent);
                            //urlObject.query.extent = projectedExtent.xmin.toFixed(4) + ',' + projectedExtent.ymin.toFixed(4) + ',' + projectedExtent.xmax.toFixed(4) + ',' + projectedExtent.ymax.toFixed(4);
                        }
                    }));

                }

            } else {
                deferred.resolve(null);
            }

            return deferred.promise;

        },
        _init: function () {
            // set sizes for select box
            this._setSizeOptions();

            var dialog = new Dialog({
                title: i18n.widgets.ShareDialog.title,
                draggable: false
            }, domConstruct.create("div"));
            this.set("dialog", dialog);

            // set embed url
            this._updateUrl();
            // select menu change
            this.own(on(this._comboBoxNode, "change", lang.hitch(this, function (evt) {
                this.set("embedWidth", this.get("embedSizes")[parseInt(evt.currentTarget.value, 10)].width);
                this.set("embedHeight", this.get("embedSizes")[parseInt(evt.currentTarget.value, 10)].height);
                this._setEmbedCode();
            })));
            // facebook click
            this.own(on(this._facebookButton, a11yclick, lang.hitch(this, function () {
                this._configureShareLink(this.get("facebookURL"));
            })));
            // twitter click
            this.own(on(this._twitterButton, a11yclick, lang.hitch(this, function () {
                this._configureShareLink(this.get("twitterURL"));
            })));
            // google plus click
            this.own(on(this._gpulsButton, a11yclick, lang.hitch(this, function () {
                this._configureShareLink(this.get("googlePlusURL"));
            })));
            // email click
            this.own(on(this._emailButton, a11yclick, lang.hitch(this, function () {
                this._configureShareLink(this.get("mailURL"), true);
            })));
            // link box click
            this.own(on(this._shareMapUrlText, a11yclick, lang.hitch(this, function () {
                this._shareMapUrlText.setSelectionRange(0, 9999);
            })));
            // link box mouseup stop for touch devices
            this.own(on(this._shareMapUrlText, "mouseup", function (evt) {
                event.stop(evt);
            }));
            // embed box click
            this.own(on(this._embedNode, a11yclick, lang.hitch(this, function () {
                this._embedNode.setSelectionRange(0, 9999);
            })));
            // embed box mouseup stop for touch devices
            this.own(on(this._embedNode, "mouseup", function (evt) {
                event.stop(evt);
            }));

            // loaded
            this.set("loaded", true);
            this.emit("load", {});
        },
        _updateEmbed: function () {
            domAttr.set(this._embedNode, "value", this.get("embed"));
        },
        _setEmbedCode: function () {
            var es = "<iframe width='" + this.get("embedWidth") + "' height='" + this.get("embedHeight") + "' src='" + this.get("url") + "' frameborder='0' scrolling='no'></iframe>";
            this.set("embed", es);
        },
        _updateBitlyUrl: function () {
            var bitly = this.get("bitlyUrl");
            if (bitly) {
                domAttr.set(this._shareMapUrlText, "value", bitly);
                domAttr.set(this._linkButton, "href", bitly);
            }
        },
        _shareLink: function () {
            if (this.get("bitlyAPI") && this.get("bitlyLogin") && this.get("bitlyKey")) {
                var currentUrl = this.get("url");
                // not already shortened
                if (currentUrl !== this._shortened) {
                    // set shortened
                    this._shortened = currentUrl;
                    // make request
                    esriRequest({
                        url: this.get("bitlyAPI"),
                        callbackParamName: "callback",
                        content: {
                            uri: currentUrl,
                            login: this.get("bitlyLogin"),
                            apiKey: this.get("bitlyKey"),
                            f: "json"
                        },
                        load: lang.hitch(this, function (response) {
                            if (response && response.data && response.data.url) {
                                this.set("bitlyUrl", response.data.url);
                            }
                        }),
                        error: function (error) {
                            console.log(error);
                        }
                    });
                }
            }
        },
        _configureShareLink: function (Link, isMail) {
            // replace strings
            var fullLink = lang.replace(Link, {
                url: encodeURIComponent(this.get("bitlyUrl") ? this.get("bitlyUrl") : this.get("url")),
                image: encodeURIComponent(this.get("image")),
                title: encodeURIComponent(this.get("title")),
                summary: encodeURIComponent(this.get("summary")),
                hashtags: encodeURIComponent(this.get("hashtags"))
            });
            // email link
            if (isMail) {
                window.location.href = fullLink;
            } else {
                window.open(fullLink, "share", true);
            }
        }
    });
    if (has("extend-esri")) {
        lang.setObject("dijit.ShareDialog", Widget, esriNS);
    }
    return Widget;
});
tscharff commented 9 years ago

Sorry for the late reply... I've been out sick for about 1.5 weeks, and still only back on a very part-time basis.

We were using a hosted template, but when I return to work I will download the code and test the ShareDialog.js that you provided above. Thanks!