Smathermather's Weblog

Remote Sensing, GIS, Ecology, and Oddball Techniques

Archive for the ‘Javascript’ Category

Leaflet Crosshairs

Posted by smathermather on February 18, 2016

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.

Posted in Javascript, Leaflet | Tagged: , , , | Leave a Comment »

OpenLayers, GeoExt, GeoServer, and GetFeatureInfo

Posted by smathermather on May 6, 2012

I wrote an earlier post on using GetFeatureInfo through OpenLayers to bring back a formatted html document with pictures, and formated tables, etc.  It wasn’t sophisticated, but got the job done.  Since around that time, as I’ve been building out our services, the speed with which a GetFeatureInfo request returns has g o t t  e  n  p    r     o     g       r       e        s         s            i             v              e             l            y     slower, to the point where the feature is essentially unusable.  Why?  Poor client coding.  Here’s my getfeatureinfo codeblock for OpenLayers:


////// WMS GetFeatureInfo
var info = new OpenLayers.Control.WMSGetFeatureInfo({
             drillDown : false,
queryVisible : true,
panMapIfOutOfView : false,
             url : GeoserverWMS,
layerUrls : [GeowebcacheURL],
eventListeners : {
getfeatureinfo : function (event) {
popup = new OpenLayers.Popup.FramedCloud(
                             "popinfo",
map.getLonLatFromPixel(event.xy),
null,
event.text,
null,
                             true);
map.addPopup(popup, true);
}
}
});

map.addControl(info);
info.activate();

//  end of popup code

The problem?  This code will query all visible layers.   Lots of visible layers results in a GetFeatureInfo request that looks like this:
localhost:8080/geoserver/wms?&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetFeatureInfo&LAYERS=summer_aerial_2,summer_aerial_1,reservation_boundaries_public_private_cm_dissolved_mask_gradien,odot_interstate,odot_us_routes,odot_state_routes,reservation_bounds,detailed_hydro_view,cm_bridge_view,cm_trails,impervious_update,cm_buildings,cm_buildings_outline,golf_view,nhd_lake_erie,supplementary_shields,planet_osm_line,cuyahoga_street_centerlines_labels,planet_osm_line_outside_cuy,detailed_hydro_labels,facilities_cm,facility_areas_cm&QUERY_LAYERS=summer_aerial_2,summer_aerial_1,reservation_boundaries_public_private_cm_dissolved_mask_gradien,odot_interstate,odot_us_routes,odot_state_routes,reservation_bounds,detailed_hydro_view,cm_bridge_view,cm_trails,impervious_update,cm_buildings,cm_buildings_outline,golf_view,nhd_lake_erie,supplementary_shields,planet_osm_line,cuyahoga_street_centerlines_labels,planet_osm_line_outside_cuy,detailed_hydro_labels,facilities_cm,facility_areas_cm&STYLES=,,,,,,,,,,,,,,,,,,,,,&BBOX=2232424.250515%2C625357.428203%2C2233363.463113%2C625592.231353&FEATURE_COUNT=10&HEIGHT=213&WIDTH=852&FORMAT=image%2Fpng&INFO_FORMAT=text%2Fhtml&SRS=EPSG%3A3734&X=320&Y=91
My naive attempts to fix this were simply adding a layers key/value pair, e.g.:

////// WMS GetFeatureInfo
     var info = new OpenLayers.Control.WMSGetFeatureInfo({
drillDown : false,
queryVisible : true,
panMapIfOutOfView : false,
             url : GeoserverWMS,
layers : [reservation_bounds],
layerUrls : [GeowebcacheURL],
eventListeners : {
getfeatureinfo : function (event) {
                     popup = new OpenLayers.Popup.FramedCloud(
                             "popinfo",
map.getLonLatFromPixel(event.xy),
null,
event.text,
null,
                             true);
map.addPopup(popup, true);
}
}
});

map.addControl(info);
info.activate();

//  end of popup code
… but for whatever reason, maybe the way I instantiated the OpenLayers.Layer.WMS instances using Ext.each (ahem, the way I modified a vendor’s cleverly written code…), the layers aren’t recognized as objects by the name I might expect.  So, I hack and create a duplicate layer by a different name.  I’ll have a non-hack version here soon (I hope) which will use the existing array of WMS instances, loop through them, and only add the appropriate ones to the layers array.  In the mean time, this should work.
var reservation_bounds = new OpenLayers.Layer.WMS("Reservation Boundaries", GeoserverWMS,
{'layers': 'base:reservation_bounds', transparent: true, format: 'image/png'},
             {isBaseLayer: false}
);

////// WMS GetFeatureInfo
var info = new OpenLayers.Control.WMSGetFeatureInfo({
drillDown : false,
queryVisible : true,
panMapIfOutOfView : false,
             url : GeoserverWMS,
layers : [reservation_bounds],
layerUrls : [GeowebcacheURL],
eventListeners : {
getfeatureinfo : function (event) {
popup = new OpenLayers.Popup.FramedCloud(
                             "popinfo",
map.getLonLatFromPixel(event.xy),
null,
event.text,
null,
                             true);
map.addPopup(popup, true);
}
}
});

map.addControl(info);
info.activate();

//  end of popup code

Posted in GeoExt, GeoServer, Javascript, OpenLayers | Tagged: , , , , | Leave a Comment »

Making a link use post instead of get

Posted by smathermather on March 10, 2012

This is such a small post, it could be a tweet, but since I re-read my blog and rarely my tweets (and as this is as much for me as for anyone else):

http://stackoverflow.com/questions/3915917/make-a-link-use-post-instead-of-get

Posted in Javascript | Tagged: , | Leave a Comment »