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>
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.