Navigating/clicking on the map to filter what is shown on the Stories page?

Hi,

Not sure if this is possible or if it is a feature request, but, when a user is on the Stories page and is viewing all the stories, would it be possible to filter the items shown if they navigated or clicked on the map? ie show as a list only the stories viewable on the map?

Thanks!

The Curatescape theme is designed around the idea that all items are location-based and will appear on the map. While we’ve considered fully supporting alternative item types, it’s not really on the agenda at this time.

Ok. Thanks for getting back to me! All the items I have in the database have locations tied to them, so no real argument there. What I was thinking was it would be nice if the user was clicking and zooming into the map, to filter the list below to show only the items that appear on the map.

Right now, user interactions and the list below are detached from each other. It might be good if there was some kind of relationship.

I think I can do this via leaflet. I just thought maybe someone else had already successfully done this :slight_smile:

Ok, I see what you mean. I think this could be a tough one once you start to have hundreds or thousands of items. The item pagination is necessary to keep the page size manageable (once upon a time, we did list all items on a single page and it got slow pretty quickly). The only option for keeping the list and the map in sync would be the reverse: to display only (e.g.) 30 items at a time on the map. And I think that would be frustrating to the user. So the map will always need to show all the items. And the list will always need to be paginated.

Ah ok. This makes sense. I see why it would be problematic. Thanks!

I seem to have solved some of the problem with connecting the map with the table below.

I added some code to some files that helps construct the SQL query so you can search for numbers greater than or less than a certain number. So I am just passing the mapBounds and checking whether an item’s lat or long fits within the bounds that a user has navigated to.

That seems ok. You can actually search, theoretically, via the map. See set of URL parameters below. (I’ve added returns before the ‘&’ so humans could read it)

This query returns a narrow set of results:

/items/browse?search=
&advanced[0][joiner]=and
&advanced[0][element_id]=55
&advanced[0][type]=is+less+than
&advanced[0][terms]=51.98318857213821
&advanced[1][joiner]=and
&advanced[1][element_id]=56
&advanced[1][type]=is+less+than
&advanced[1][terms]=-0.40374755859375006
&advanced[2][joiner]=and
&advanced[2][element_id]=55
&advanced[2][type]=is+greater+than
&advanced[2][terms]=51.28940590271679
&advanced[3][joiner]=and
&advanced[3][element_id]=56
&advanced[3][type]=is+greater+than
&advanced[3][terms]=-3.8369750976562504
&user=
&tags=
&featured=
&mapSearch=true
&submit_search=Search+for+items

While this query show the entire scope of objects:

/items/browse?search=
&advanced[0][joiner]=and
&advanced[0][element_id]=55
&advanced[0][type]=is+less+than
&advanced[0][terms]=52.7999868
&advanced[1][joiner]=and
&advanced[1][element_id]=56
&advanced[1][type]=is+less+than
&advanced[1][terms]=16.36388589647
&advanced[2][joiner]=and
&advanced[2][element_id]=55
&advanced[2][type]=is+greater+than
&advanced[2][terms]=48.2049038
&advanced[3][joiner]=and
&advanced[3][element_id]=56
&advanced[3][type]=is+greater+than
&advanced[3][terms]=-2.1208137
&user=
&tags=
&featured=
&mapSearch=true
&submit_search=Search+for+items

Right now, loading via ajax, the same page, turning off what I can turn off, and, it’s generally a mess.

I originally thought I might be able to point to an entirely new page like “browse-table-only.php” and just get the stuff from there, but it actually returns a 404 in the browser.

Do I need to register that page somewhere? I’m not sure how these themes work in Omeka.

Or can I continue to use “browse.php” but exclude more of the repetitive inclusions (essentially all the included files and js and css are showing up again)? Is that easier?

It’s not technically broken the way it is now. It’s just, as they say, suboptimal.

Any guidance would be great.

Will share the specific things I changed in a separate post underneath this one.

Thanks!

Updated code and documentation below :slight_smile:

Hi @thejovijuan, can you send a link or screen capture showing the result?

Sure. Here are a few. The first one is the default, all items view screen

and the second, when you navigate to London.

I’m still working on the code. The previous and next buttons were messed up (done) and I need to update the map when you go back to the page/bookmark it (not done). Argh. Leaflet setView is not taking the center,zoom variables I’m passing it.

I grabbed the URL from your code so I could have a closer look. Here are a few thoughts.

I noticed that zooming out does not reset the list and some of the clusters won’t expand (although I think that’s because they have the exact same coordinates; so you might want to turn on spiderfy – see docs). I think if you continue in this direction, I’d recommend just turning off or suppressing pagination. Switching from All Items to Mapped Items is potentially confusing, so you might as well just show Mapped Items as the default and maybe add some guidance on how to use the map to refine the list. Or maybe it would be better to have this as an option that users can turn on and off. There’s a lot to think about on this one!

FWIW, even though I think this is a pretty cool solution, I’m not sure we’d accept a pull request for this feature. But that doesn’t mean you should stop working on it. Keep us posted here.

Also note that there are probably going to be accessibility concerns with using the map as a core navigation option. Right now, we treat it as a secondary/supplemental method of navigation. So if you’re using keyboard controls (e.g. tab), you’re offered the option to skip over the map (see screenshot below). If the map becomes the primary navigation option, you might want to rethink that and also work on improving the accessibility of the clusters (which don’t currently work with keyboard controls).

Thanks so much for taking a look Erin. I really appreciate it.

Whoops. I should have nixed that URL. Oh well. It’s not a huge deal. We’re just still working on it and don’t want it to spread too far.

Response to some of your thoughtful queries below…

I fixed all the resetting issues, so it should be using the map as filtering system now. I turned on spiderfy (it was off by default, with a comment asking whether it should be settable in the tool. I would say a hearty yes to that!).

I will change default header to “Mapped Items” rather than “All Items”. Makes more sense.

I think it’s ok the way it is. The logic makes sense ie Here’s a map with all the items, and here below is a paginated table of all the items on the map. I’m just trying to make the map and table relate to each other more strongly.

I feel the map is not really the primary nav. Both the map and the table are navigation tools, but they are probably equally weighted. Maybe weighted more toward the map, but that’s because it’s placed first, as it is in the default theme. And accessibility challenged users can still skip over it, so I think we are ok there too,

Will post some improvements later as a complete post. Right now, I got it to ajax in the table on previous and next. Much snappier. I’m surprised that isn’t the default Omeka behaviour.

Thanks again!

First off, you need to have Latitude and Longitude added to whatever Item Type you are using. I couldn’t figure out how to get the geolocation stuff associated with the item except through this method, but maybe someone else can.

To handle the extra search parameters you need to do some groundwork.

In themes/curatescape/items/search.php, you should probably add the “is greater than” and “is less than” to the drop down. It’s not strictly necessary, but can help if you are debugging. You can remove these before publishing as no user in their right mind would use this feature:

add at line 101:

					    echo $this->formSelect(
		                    "advanced[$i][type]",
		                    @$rows['type'],
		                    array(
		                        'title' => __("Search Type"),
		                        'id' => 'id-advanced-search-type',
		                        'class' => 'advanced-search-type'
		                    ),
		                    label_table_options(array(
		                        'contains' => __('contains'),
		                        'does not contain' => __('does not contain'),
		                        'is exactly' => __('is exactly'),
		                        'is empty' => __('is empty'),
		                        'is not empty' => __('is not empty'),
		                        'starts with' => __('starts with'),
		                        'ends with' => __('ends with'),
		                        'is greater than'=> __('is greater than'),
		                        'is less than'=> __('is less than'))
		                    )
		                );	

For the actual working bit, you need to go to /application/models/Table/item.php

Add at around line 142 (it’s a big “case” statement):

			    case 'is greater than':
					$predicate= ">" . $db->quote($value, Zend_Db::FLOAT_TYPE);
					break;
				case 'is less than':
					$predicate = "<" . $db->quote($value, Zend_Db::FLOAT_TYPE);
					break;

From there, you need to make changes to /themes/curatescape/items/custom.php

At around line 433, you need to add a few javascript functions that will be used here and in browse.php. Insert these after var isSecure is set.

//make map a global for debugging and to use in later functions
		var map;
		
		//jquery to rewrite links on previous/next buttons
		function rewritePrevAndNextBtns(){
			var rawUrlPrevious=$(".pagination_previous").find("a").attr('href');
			var rawUrlNext=$(".pagination_next").find("a").attr('href');
			//console.log("rawUrlNext:"+rawUrlNext);
			if(rawUrlPrevious){
				if (rawUrlPrevious.indexOf('&tableReset=1')>-1){
					//cleanUrl=rawUrl.replace('&tableReset=1','');
					//console.log(cleanUrl);
					var newJs="javascript:update_table_only('"+rawUrlPrevious+"')";
					$(".pagination_previous").find("a").attr('href',newJs);
				}
			}
			if(rawUrlNext){
				if (rawUrlNext.indexOf('&tableReset=1')>-1){
					//cleanUrl=rawUrl.replace('&tableReset=1','');
					//console.log(cleanUrl);
					var newJs="javascript:update_table_only('"+rawUrlNext+"')";
					$(".pagination_next").find("a").attr('href',newJs);
				}
			}
		}
	
	
		//ajaxing just the table
		function update_table_only(myURL,myParams){
			//console.log("myURL:"+myURL)
			if(myURL.indexOf("?")>-1){
				myParams=myURL.split("?")[1];
			}
			//console.log("myParams:"+myParams);
			$.ajax({
				url: myURL,
				method: 'GET',
				dataType: 'html',
				data: myParams,
				//figure out what function to call
				success: function (response) {
					var $newPage=$("article").html(response);
					$newPage.find("header").hide();
					$newPage.find("#admin-bar").hide();
					$newPage.find("#wrap").css("margin-top",0);
					$newPage.find("#wrap").css("padding",0);
					$newPage.find("#wrap").css("margin",0);
					$newPage.find("#wrap").css("box-shadow","0 0 0px #333");
					$newPage.find(".browse").css("padding",0);
					var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?'+myParams.replace('&tableReset=1','');
					window.history.pushState({ path: newurl }, '', newurl);
				},
				complete: function() {
					//$(panel).removeClass('loading');
			
						rewritePrevAndNextBtns();
			
			
				}
			});
		}
		
		function updateView(){
			var myQueryString = window.location.search;
			console.log("myQueryString:"+myQueryString);
			var myUrlParams = new URLSearchParams(myQueryString);
			var center1=myUrlParams.get('center');
			if(!center1){return}
			var center2=center1.split(",");
			console.log("center2:"+center1);
			console.log("center2[0]:"+center2[0])
			var center=Array(((center2[0])*1),((center2[1])*1));
			var zoom=(myUrlParams.get('zoom'))*1;
			console.log("center:"+center);
			if(center){
				console.log("center:"+center);
				console.log("zoom:"+zoom);
				setTimeout(function(){
					map.setView(center,zoom);
				},500)

			}
		}

At the top of the mapDisplay function, replace the initial view with the code below. No need to have the ‘var’ marker in front of ‘map’ now as it is defined and used above. Adding the updateView() function is the key bit here.

map = L.map('curatescape-map-canvas',{
						layers: defaultMapLayer,
						minZoom: 3,
						scrollWheelZoom: false,
					}).setView(center, zoom);
					console.log("center:"+center);
					console.log("zoom:"+zoom);
					
					//check if view is set in query string...
					updateView();

From line 628, on my document, right after the function map.on(‘popupopen’, function(e)
within the mapDisplay function.

//flag to filter out system actions from user initiated interactions
				$('.curatescape-map').children().on('click drag mouseover mouseenter dblclick touchend wheel',function(e){
					clickedMapFlag=true;
				});
				
				$('.leaflet-control-zoom').children().on('click drag mouseover mouseenter dblclick touchend wheel',function(e){
					clickedMapFlag=true;
				});
					
					//detecting interactions with map
					map.on('moveend',function(e){
						checkMapMovement();
					});
					
					map.on('zoomend',function(e){
						checkMapMovement();
					});
					
					//function to user's map movement
					function checkMapMovement(){
						console.log("changing map view, clickedMapFlag="+clickedMapFlag);
						mapBounds = map.getBounds();
						if(clickedMapFlag==true){
							//setTimeout(function(){
								//mapBounds = map.getBounds();
								getStuffOnMap();
								clickedMapFlag=false;
							//},1000)
							
						}
					}
					
					//key function to filter the table by what's on the map
					function getStuffOnMap(){
						//construct the query
						var lat1= "search=&advanced[0][joiner]=and&advanced[0][element_id]=55&advanced[0][type]=is+less+than&advanced[0][terms]="+mapBounds._northEast.lat;
						var long1=						"&advanced[1][joiner]=and&advanced[1][element_id]=56&advanced[1][type]=is+less+than&advanced[1][terms]="+mapBounds._northEast.lng;
						var lat2= "&advanced[2][joiner]=and&advanced[2][element_id]=55&advanced[2][type]=is+greater+than&advanced[2][terms]="+mapBounds._southWest.lat;
						var long2=						"&advanced[3][joiner]=and&advanced[3][element_id]=56&advanced[3][type]=is+greater+than&advanced[3][terms]="+mapBounds._southWest.lng; 
						/*
						below is some horrible, horrible leaflet crap I needed to deal with
						both here and in updateView()
						Hours of my life went down this hole.
						Essentially you need to transform objects that leaflet returns in 
						its functions (looking at you getCenter()) for use in other
						leaflet functions. Arghhhh. 
						*/
						//viewRedo:(LatLng(51.68959, -0.8226),9)<-- what getCenter returns
						var myMapCenter=String(map.getCenter());
						//console.log("myMapCenter:"+myMapCenter);
						//var myView="&view=" + myMapCenter.replace("LatLng(","[").replace(")","]").replace(" ","") +"," + map.getZoom();
						var myView="&center=" + myMapCenter.replace("LatLng(","").replace(")","").replace(" ","") +"&zoom=" + map.getZoom();
						//console.log("myView:"+myView);
						
						var trailingStuff="&user=&tags=&featured=&tableReset=1&submit_search=Search+for+items";
						var myParams=lat1+long1+lat2+long2+myView+trailingStuff;
						var myURLShorter="/mapping/items/browse/";
						//I deleted the full URL above :). the relative link should work as well, but the full one is what I used.
						//write ajax request to get results to overwrite search table
						$.ajax({
							url: myURLShorter,
							method: 'GET',
							dataType: 'html',
							data: myParams,
							success: function (response) {
								var $newPage=$("article").html(response);
								//need to rewrite some css so the table is clean
								$newPage.find("header").hide();
								$newPage.find("#admin-bar").hide();
								$newPage.find("#wrap").css("margin-top",0);
								$newPage.find("#wrap").css("padding",0);
								$newPage.find("#wrap").css("margin",0);
								$newPage.find("#wrap").css("box-shadow","0 0 0px #333");
								$newPage.find(".browse").css("padding",0);
								
								//need to replace the tableReset variable so you don't 
								//just get the table if you've bookmarked the page
								var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?'+myParams.replace('&tableReset=1','');
								console.log("newurl:"+newurl);
								window.history.pushState({ path: newurl }, '', newurl);
							},
							complete: function() {
								rewritePrevAndNextBtns();
							}
						});
					}

Changes to /themes/curatescape/items/browse.php
You need to check $tableReset at the top. this is set in getStuffOnMap()
Added at line 11.

//added
$tableReset=isset($_GET['tableReset']);
//end added

In the big if statement at the top, after elseif($query), add this and replace the else statement, around line 48:

elseif($tableReset){
	$title = __('Mapped %s', mh_item_label('plural'));
	//$bodyclass .=' items stories';
	$bodyclass .=' queryresults';
	$maptype='queryresults';
}
else{
	$title = __('Mapped %s', mh_item_label('plural'));
	$bodyclass .=' items stories';
}

At line 61 add this stuff which prevents the header stuff from being written twice:

if	(!($tableReset)) {
	echo head(array('maptype'=>$maptype,'title'=>$title,'bodyid'=>'items','bodyclass'=>$bodyclass));
}
	$wroteTable=false;
(wroteTable is another flag to stop the page from writing multiple tables)	

On line 69 I added a function to write the table. I’m not sure it’s actually necessary, but the table was writing multiple times for some reason and this controls for that situation.

<?php

function writeTable($title,$total_results){
	//print 'title=' . $title;
	
	if($wroteTable==false){
		
		?> 
		
		<h2 class="query-header">
			<?php 
				$title .= ( $total_results  ? ': <span class="item-number">'.$total_results.'</span>' : ': 0');
				echo $title; 
			?>
		</h2>
		<div id="primary" class="browse">
			<section id="results">
				<h2 hidden class="hidden"><?php echo mh_item_label('plural');?></h2>
				
				<nav class="secondary-nav" id="item-browse"> 
					<?php echo mh_item_browse_subnav();?>
				</nav>
			
				<div class="pagination top"><?php echo pagination_links(); ?></div>
				<div class="browse-items flex" role="main">
				<?php 
				foreach(loop('Items') as $item): 
					$item_image=null;
					$tags=tag_string(get_current_record('item') , url('items/browse'));
					$hasImage=metadata($item, 'has thumbnail');
					if ($hasImage){
							preg_match('/<img(.*)src(.*)=(.*)"(.*)"/U', item_image('fullsize'), $result);
							$item_image = array_pop($result);				
					}else{
						$item_image='';
					}
				
					?>
					<article class="item-result <?php echo $hasImage ? 'has-image' : 'no-image';?>">
						<?php echo link_to_item('<span class="item-image" style="background-image:url('.$item_image.');" role="img" aria-label="'.metadata($item, array('Dublin Core', 'Title')).'"></span>',array('title'=>metadata($item,array('Dublin Core','Title')))); ?>
						<h3><?php echo mh_the_title_expanded($item); ?></h3>
						<div class="browse-meta-top"><?php echo mh_the_byline($item,false);?></div>
					
					
						<div class="item-description">
							<?php echo mh_snippet_expanded($item); ?>
						</div>
					
						<?php if(false): /* TODO: make a theme option */ ?>
							<div class="item-meta-browse">
								<?php 
								if(get_theme_option('subjects_on_browse')==1){
									echo mh_subjects(); 
									}
								?>					
								<?php echo mh_tags();?>
							</div>
						<?php endif;?>
					
					</article> 
				<?php endforeach; ?>
			
				<?php if($query && !$total_results){?>
				<div id="no-results">
					<p><?php echo ($query) ? '<em>'.__('Your query returned <strong>no results</strong>.').'</em>' : null;?></p>
					<?php echo search_form(array('show_advanced'=>true));?>
				</div>
				<?php }?>
			
				</div>
				<div class="pagination bottom"><?php echo pagination_links(); ?></div>
			</section>	
		</div><!-- end primary -->
	
		<?
		}
		$wroteTable=true;
		
	}


?>

Finally, you have to wrap a bunch of stuff in conditionals so it won’t write if $tableReset is true.
At line 156 to the end of the document, replace everything with this:

<div id="content">
	<?php
		if(!($tableReset)){
		?>
		<section class="map">
				<h2 hidden class="hidden"><?php echo __('Map');?></h2>
				<nav aria-label="<?php echo __('Skip Interactive Map');?>"><a id="skip-map" href="#primary"><?php echo __('Skip Interactive Map');?></a></nav>
				<figure>
					<?php echo mh_map_type($maptype,null,null); ?>
				</figure>
		</section>
		<?
		}
	?>
<article class="browse stories items">	
	<?php echo writeTable($title,$total_results);?>
	
 	<?php
		if(!$tableReset){
		mh_share_this();
		
		}
	?>
</article>
</div> <!-- end content -->
	<?php
	if(!$tableReset){
		foot();
	}
	?>
<script>
	//this is to remove the tableReset=1 variable from the dynamically generated links
	$(document).ready(function(){
			rewritePrevAndNextBtns();
		//$(".pagination").find("a").attr('href').replace('tableReset=1','')
	});
	
	
	</script>

I think that’s all. Remember to commit all your stuff to git so you can roll back if something breaks.

Please excuse by excessing commenting and console.logging and the sometimes unhelpful rants therein.

Let me know on this thread if you try it and it works (or, more importantly, if it doesn’t).