Leaflet Crosshairs

Ok, I’m not the first to come up with that name, but I like it so it’s staying for now.

The problem space is this: Your web development team is good with forms. They build forms like diurnal animals wake — daily: day in, day out, day in, day out. They want to build a form for a mobile web app and it just happens to use HTML5 to grab the geolocation from field deployed phones, tablets, etc.. Great! Geo problem solved — when we collect our form data, we’ll get geo-data for free.

Not so fast, form-building Valentine — what’s the quality of those data? How accurate is that phone GPS? Is it good enough? The answer is probably yes, most of the time.

But, good enough most of the time isn’t good enough, Faye. What I want is an embedded map where I can move the crosshairs to the actual location inside the form. It’s like all the corrections you do back at the office, but you can do them in the field while the site is still fresh in your mind. Simple. Useful. (Also, infrastructure like Fulcrum App does it because it’s so simple and useful). Hence, questions like this: http://gis.stackexchange.com/questions/90225/how-to-add-a-floating-crosshairs-icon-above-leaflet-map and pages like this: https://www.mapbox.com/blog/help-search-MH370/

I couldn’t get the solution on Stack Exchange to work for me. Besides, I think its the wrong solution. I don’t want to move the icon back to center on the map moving, I want the map to move and the icon to stay stationary. It’s a fine (and probably irrelevant) distinction, but it feels important to me.

So, we build a small page that has the following features:

  • A crosshairs that is stationary
  • A map that moves
  • When the map moves, our lat/lon values update in our form

Main code as follows (careful — careless use of jquery follows):

<!DOCTYPE html>
<html>
<head>
	<title>Leaflet Crosshairs</title>

	<!-- Include meta tag to ensure proper rendering and touch zooming -->
	<!--<meta name="viewport" content="width=device-width, initial-scale=1" />-->
	<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

	<!-- Include the jQuery library -->
	<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
	<!-- Include jQuery Mobile stylesheets -->
	<link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.css">
	<!-- Include the jQuery Mobile library -->
	<script src="//ajax.googleapis.com/ajax/libs/jquerymobile/1.4.5/jquery.mobile.min.js"></script>

	<!-- Include leaflet css and js -->
	<link rel="stylesheet" href="//cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css" />
	<script src="//cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js"></script>

	<style>
	body {
		padding: 0;
		margin: 0;
	}
        html, body, #map {
            height: 100%;
            width:100%;
        }

        #metamap {
            width: 100%;
            height: 300px;
        }
        #crosshair {
            position: relative;
            z-index: 10;
            height: 200px;
            vertical-align: middle;
        }
	#crosshair img {
		position: absolute;
		margin: 0;
		top: 50%;
		left: 50%;
		margin-right: -50%;
		transform: translate(-50%, -50%);
	}
	</style>
</head>
<body>

    <div id="metamap">
        <div id="map">
            <div id="crosshair"><img class="crosshair" src=crosshair.png /></div>
        </div>
    </div>
    <br />
    <hr />
    Latitude: <input type="text" id="txtLatitude" />
    <br /><br />
    Longitude: <input type="text" id="txtLongitude" />
    
        <script>
            // Initiate map
            var map = L.map('map');

            // load map
            L.tileLayer('//api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiY2xldmVsYW5kLW1ldHJvcGFya3MiLCJhIjoiWHRKaDhuRSJ9.FGqNSOHwiCr2dmTH2JTMAA', {
                maxZoom: 20,
                id: 'mapbox.satellite'
            }).addTo(map);

            // Now a function to populate our form with latitude and longitude values
            function onMapMove(e) {
                // txtLatitude.val(map.getCenter());
                var locale = map.getCenter();
                $('#txtLatitude').val(locale.lat);
                $('#txtLongitude').val(locale.lng);
            }
            
            // Boilerplate...
            function onLocationError(e) {
                alert(e.message);
            }

            // When the map moves we run our function up above
            map.on('move', onMapMove);

            // Boilerplate
            map.on('locationerror', onLocationError);
            
            // When we load the map, we should zoom to our current position using device geolocation
            map.locate({ setView: true, maxZoom: 20 });
        </script>
</body>
</html>

<body>


<div id="metamap">

<div id="map">

<div id="crosshair"><img class="crosshair" src=crosshair.png /></div>

        </div>

    </div>

    

<hr />

    Latitude: <input type="text" id="txtLatitude" />
    

    Longitude: <input type="text" id="txtLongitude" />
    
        <script>
            // Initiate map
            var map = L.map('map');

            // load map
            L.tileLayer('//api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiY2xldmVsYW5kLW1ldHJvcGFya3MiLCJhIjoiWHRKaDhuRSJ9.FGqNSOHwiCr2dmTH2JTMAA', {
                maxZoom: 20,
                id: 'mapbox.satellite'
            }).addTo(map);

            // Now a function to populate our form with latitude and longitude values
            function onMapMove(e) {
                // txtLatitude.val(map.getCenter());
                var locale = map.getCenter();
                $('#txtLatitude').val(locale.lat);
                $('#txtLongitude').val(locale.lng);
            }
            
            // Boilerplate...
            function onLocationError(e) {
                alert(e.message);
            }

            // When the map moves we run our function up above
            map.on('move', onMapMove);

            // Boilerplate
            map.on('locationerror', onLocationError);
            
            // When we load the map, we should zoom to our current position using device geolocation
            map.locate({ setView: true, maxZoom: 20 });
        </script>
</body>
</html>

Screen shot of app in action

Things to fix:

  • Alignment of crosshairs so they are properly centered
  • Better looking crosshairs
  • Rounding for those coordinate values
  • Do we need jQuery? Pro’ly not

That was my fun for the day. Shout-out to Tanetta Jordan my brilliant paired programmer for the day. Without her, this would have taken a week… .

Oh ya, and git-repo here: https://github.com/cleveland-metroparks/leaflet-crosshairs

Look there for updates that should include the improvements above.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.