javascript

A 2-post collection

"Zooming" into a map element using element coordinates

Written by Michael Earls
 development  javascript  GIS  DevExtreme  DevExpress  Mapping

I am still working on a map at work and I needed to be able to zoom into a map element when a user clicks on it. Originally, I was using the dxVectorMap.zoomLevel() method, but I realized that calculating the zoom level was harder than I thought.

Note: I covered the basics of working with the dxVectorMap in a previous post titled Working with Geographical data using Dev Extreme dxVectorMap

I then noticed that each map element has a coordinates() method that you can call that will return an array of points for the boundaries of the element. It uses those points to draw the boundary lines on the map.

So, I decided to write a simple two-dimensional loop to calculate the minimum and maximum latitude and longitude points on the element.

The way this works is that you loop through each point to find the largest latitude, the smallest latitude, the largest longitude, and the smallest longitude in the array and use these to build your bounds array.

Here is a simplified image to help describe the process:

Map boundary calculation demonstration

In the picture above, all of the red colored points ([50,1],[90,20], [1,60], and [52,95]) are the max and min values for their latitude and longitude. In this simple example, there are only four points that make up the boundaries.

The remaining points are unimportant as they are within the boundary of the other four major points.

Our goal in the code is to locate the extreme edges (the red points in our picture). To do this, we have to do some comparisons during our iteration of the array of 2D points. As I loop through the array of point arrays, there will be a massive number of points for this shape. We need to have 4 variables to keep up with our min and max values (min latitude, max latitude, min longitude, and max longitude). As we loop through the array, we'll check to see if the new value is higher than our highest and lower than our lowest values and store them accordingly.

Here is the code:

 var _cerkit = {  
    getElementBounds: function (elementCoordinates) {
        // loop through the element coordinates and find the bounds for the eastern-, western-, southern-, and northern-most points
        // return them as the bounds (can be used to set the viewport) and zoom level

        // prep the min and max values (with opposite values so we can compare min and max properly)
        var minLongitude = 180;
        var maxLongitude = -180;
        var minLatitude = 90;
        var maxLatitude = -90;

        for (var i = 0; i < elementCoordinates.length; i++) {
            for (var j = 0; j < elementCoordinates[i].length; j++) {

                // retrieve the latitude and longitude from the element points array
                var lng = elementCoordinates[i][j][0];
                var lat = elementCoordinates[i][j][1];

                if (lng > maxLongitude) {
                    maxLongitude = lng;
                } else if (lng < minLongitude) {
                    minLongitude = lng;
                }

                if (lat > maxLatitude) {
                    maxLatitude = lat
                } else if (lat < minLatitude) {
                    minLatitude = lat;
                }
            }
        }

        // return bounds (with adjustments for borders)
        return [(minLongitude - 0.05), (maxLatitude + 0.05), (maxLongitude  + 0.05), (minLatitude - 0.05)];
    }
};

After running this code on the shape represented by the picture above, our boundaries will be (dxVectorMap coordinates are ordered [longitude,latitude]):

min latitude = 1
min longitude = 1
max latitude = 95
max longitude = 90

So, our bounds would be (arranged according to the requirements of dxVectorMap, which are longitude first):

Order:

[minLongitude, maxLatitude, maxLongitude, minLatitude]

Example map

var sampleImageBounds = [[1,1],[1,90],[95,1],[95,90]];

Also, I adjusted each of the values by 0.05 to allow for space between the edges of the map element and the boundary. 0.05 is an arbitrary number that I picked that works well with my map at my zoom level. You may need to adjust this value according to the zoom level and boundary needs of your map.

Then, simply pass this array to a call to viewport() on the dxVectorMap widget:

$("mapContainer").dxVectorMap("instance").viewport(_cerkit.getElementBounds(selectedDistrict.coordinates()));

See the dxVectorMap documentation for more information on using coordinates() and viewport().

Another approach to adding icons to dynamically generated links

Written by Michael Earls
 bootstrap  programming  icons  jquery  javascript

In my previous post, I outlined the code necessary to add icons to navigation links. I have since updated my implementation to use a function to add the link-to-icon mapping.

I have refactored my theme code to use the new approach.

The GitHub repository is ghost-cerkit-theme.

Instead of using JSON to define the link-to-icon mapping (as outlined in the previous post), we use an object on the window object:

window.linkIconMap = {};

Then, we define a function that we can use to add a new mapping:

window.addLinkIcon = function (target, icon, size) {  
    // check to see if we have maps defined already
    if (!window.linkIconMap.maps) {
        // if not, define it
        window.linkIconMap.maps = [];
    }

    // if we have a size passed in, use it, otherwise use the default icon size on our icon map. If that's missing, use nothing (Font Awesome default size)
    var iconSize = size ? size : 'defaultIconSize' in window.linkIconMap ? window.linkIconMap.defaultIconSize : '';
    window.linkIconMap.maps.push({ "target": target, "icon": icon, "size": iconSize });
};

First, we see if our maps array exists. If not, we create it. This prevents us from doing anything if the theme user never added any mappings.

After we check on the maps, we figure out what size to use. Basically, the same rules apply as before: if the map does not define a size, then we use the default size stored in defaultIconSize on our window.linkIconMap (but only if it's defined).

// sample definition for icon size
window.linkIconMap.defaultIconSize = 'fa-lg';

As a last resort, we fall back on an empty string, which has the effect of using the default size from the Font Awesome font.

In order for the theme user to create the map, they simply call the window.addLinkIcon() function:

// Navbar Icon Map  
window.addLinkIcon('nav-home', 'fa-home');  
window.addLinkIcon('nav-about', 'fa-user');  
window.addLinkIcon('nav-my-public-key', 'fa-key');  
window.addLinkIcon('nav-test', 'fa-cogs', 'fa-2x' /* optional */);

Note: you may notice that it's possible to bind an icon to any class on your page, not just navigation links. For example, I just set up my navbar expansion button (displayed when the screen is too narrow to show all the buttons) so that it does not have anything in it. I then add a call to window.addLinkIcon('navbar-toggle', 'fa-sitemap', 'fa-2x'); for it. This will then apply to all navbar toggles.

Here is where the toggle button is defined.

When the page loads, it calls the following code:

function bindLinkIcons() {  
    if (window.linkIconMap.maps) {
        var curIconMap;
        var curSize;

        for (var i = 0; i < window.linkIconMap.maps.length; i++) {
            // get a handle on the current icon map
            curIconMap = linkIconMap.maps[i];

            // set the icon on the navbar item
            createIcon(curIconMap.target, curIconMap.icon, curIconMap.size);
        }
    } else {
        console.warn('cerkit-bootstrap theme supports navbar link icons. Add the following to your footer in code injection: \<script\>window.addLinkIcon(/* target = */ "nav-home", /* icon = */ "fa-home", /* (optional) size = */ "fa-3x");\</script\>');
    }
}

$(bindLinkIcons);

We're only going to bind the icons if we have a map array to work with. If we do, then we simply loop through each entry and call the createIcon() function, passing in the relevant information.

Here is the createIcon() function definition:

function createIcon(target, icon, size) {  
    var iconElement = $(document.createElement('i')).attr('class', 'link-icon fa fa-fw ' + icon + ' ' + size).append('&nbsp;');
    var targetNavbarItem = $('.' + target);
    var targetItemFirstChild = $(targetNavbarItem).children()[0];

    // figure out if the nav item has any links in it. If so, use that as the icon parent.
    // Otherwise, use the navbarIconItem.
    var iconParentElement = targetItemFirstChild == null ? targetNavbarItem : targetItemFirstChild;

    // insert the icon element at the beginning of the parent
    $(iconParentElement).prepend(iconElement);
}

The first thing we do is create a new i tag to use as the icon container element. We then add the appropriate class attributes based on the icon and size values passed into the function. After the icon is defined, we append a non breaking space so that the icon isn't too close to the link. We then add the link-icon class to each of the icons that gets bound to a link. Then, in the site's css file, we simply add the following code:

.link-icon {  
    margin-right: 3px;
}

The next thing that happens is the code gets a handle on the target by selecting it based on the class name passed into the function (as the target argument).

Then, the first child element is selected (it is assumed that this element will either be a a tag or plain text. Whatever it is, we'll prepend our new i element representing our icon.

I believe that this approach is superior to the original approach as it is friendlier and more expressive to call a function to add a mapping rather than some arbitrary (and potentially confusing) JSON code.

I have created a Gist file that contains the core code to make this work.