Skip to content

Commit

Permalink
Merge branch 'EMC-38-spatial-search-ui' into 'develop'
Browse files Browse the repository at this point in the history
Added spatial searching

See merge request eip/catalogue!596
  • Loading branch information
rodscott committed Feb 22, 2024
2 parents 3c9a7f7 + dd93fcb commit bf34bde
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,22 @@ public String toUrl() {
}

// cannot just encode UriComponents as other parameters (facets, bbox) already Uri encoded
return builder.build().toUriString();
// Note: if a querystring exists (denoted by '?'), then add a '#' after it to turn the whole querystring into
// a backbone hash fragment (https://backbonejs.org/). This makes the search filters all persist via the url, without this then facet filters would overrule the spatial filter on the interface.
// Also, there may be a more elegant way to do this since UriComponentsBuilder has support for fragments, but the current solution is very simple and effective
return builder.build().toUriString().replace("?", "?#");

}

/**
* The bounding boxes of a single metadata record are indexed in solr in the field 'locations'
* Using this 'locations' field, the simplest spatial query to find the records within a search box is: 'locations:"Intersects(ENVELOPE(minX, maxX, maxY, minY)"
* @bbox: a bounding box string of the form: minX,maxX,maxY,minY
* @spatialOperation: one of two values defined in SpatialOperation that defines what spatial operation to perform
*/
private void setSpatialFilter(SolrQuery query) {
if(bbox != null) {
query.addFilterQuery(String.format("locations:\"%s(%s)\"", spatialOperation.getOperation(), bbox));
query.addFilterQuery(String.format("locations:\"%s(ENVELOPE(%s))\"", spatialOperation.getOperation(), bbox));
}
}
private void setRecordVisibility(SolrQuery query) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package uk.ac.ceh.gateway.catalogue.search;

public enum SpatialOperation {
INTERSECTS ("Intersects"),
ISWITHIN ("IsWithin");
INTERSECTS ("intersects"),
ISWITHIN ("iswithin");

private final String operation;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ public void noExceptionThrownWhenBBoxIsValid() {
SolrQuery solrQuery = query.build();

//Then
assertThat(Arrays.asList(solrQuery.getFilterQueries()).contains("locations:\"IsWithin(1.11,2.22,3.33,4.44)\""), is(true));
assertThat(Arrays.asList(solrQuery.getFilterQueries()).contains("locations:\"iswithin(ENVELOPE(1.11,2.22,3.33,4.44))\""), is(true));
}

@Test
Expand Down Expand Up @@ -379,7 +379,7 @@ public void canSetIntersectBBox() {
SolrQuery solrQuery = query.build();

//Then
assertThat(Arrays.asList(solrQuery.getFilterQueries()).contains("locations:\"Intersects(1.11,2.22,3.33,4.44)\""), is(true));
assertThat(Arrays.asList(solrQuery.getFilterQueries()).contains("locations:\"intersects(ENVELOPE(1.11,2.22,3.33,4.44))\""), is(true));
}

@Test
Expand Down Expand Up @@ -574,7 +574,7 @@ public void checkThatCompleteUrlIsGenerated() {
//Then
assertThat("Term should be searched for", url, containsString("term=My+Search+Term"));
assertThat("BBOX should be searched for", url, containsString("bbox=1,2,3,4"));
assertThat("OP should be present", url, containsString("op=IsWithin"));
assertThat("OP should be present", url, containsString("op=iswithin"));
assertThat("page should be specified", url, containsString("page=24"));
assertThat("rows should be present", url, containsString("rows=30"));
assertThat("facet should be filtered", url, containsString("facet=licence%7Cb"));
Expand Down Expand Up @@ -609,7 +609,7 @@ public void checkUrlIsGenerated() {
String url = interestingQuery.toUrl();

//Then
assertThat(url, equalTo("http://my.endpo.int?page=24&rows=30&term=My+Search+Term&bbox=1,2,3,4&op=IsWithin&facet=licence%7Cb"));
assertThat(url, equalTo("http://my.endpo.int?#page=24&rows=30&term=My+Search+Term&bbox=1,2,3,4&op=iswithin&facet=licence%7Cb"));
}

@Test
Expand Down
15 changes: 10 additions & 5 deletions templates/html/search.ftlh
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
<#import "skeleton.ftlh" as skeleton>
<#assign docroot="documents">

<@skeleton.master title="Search" catalogue=catalogue searching=true containerClass="container-fluid" >
<div class="search facets-mode" id="search">

<div class="filters-panel">
<div class="header facet-heading">
<h3>Filters</h3>
</div>
<div class="facet-filter"><#include "search/_facets.ftlh"></div>
<div class="header facet-heading">
<h3>Facet filters</h3>
</div>
<div class="facet-filter"><#include "search/_facets.ftlh"></div>
<div class="spatial">
<div class="header facet-heading">
<h3>Spatial filter</h3>
</div>
<div class="spatial-filter"></div>
</div>
</div>

<div class="results"><#include "search/_page.ftlh"></div>
Expand Down
45 changes: 41 additions & 4 deletions web/less/search.less
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
//Hide the filters panel in small screen mode
.filters-panel {
height: 0; // WARNING: We would usually just set display:none to
overflow: hidden; // hide the panel. This causes OpenLayers to fail to load
overflow: auto;
//scrollbar-gutter: stable;
scrollbar-width: thin;
}

@media (min-width: @screen-md-min) {
Expand All @@ -29,6 +31,7 @@
font-size: 1.2em;
color: @filter-body-colour;
}

.header {
height: @filter-heading-height;
width: 100%;
Expand All @@ -41,6 +44,7 @@
font-size: 1.3em;
color: @filter-heading-color; //@header-text;
cursor: pointer;

&:hover {
text-decoration: underline;
}
Expand All @@ -51,12 +55,35 @@
#drawing-toggle {
display: none;
}

.map-heading {
position: absolute;
bottom: 0;
}

.map {
height: 250px;
}

.spatial-filter {
// hide the Save and Cancel buttons from the draw actions (https://stackoverflow.com/questions/51360283/how-to-remove-save-option-from-leaflet-draw-api-delete-button)
ul.leaflet-draw-actions.leaflet-draw-actions-bottom li a[title="Save changes"],
ul.leaflet-draw-actions.leaflet-draw-actions-bottom li a[title="Cancel editing, discards all changes"] {
display: none;
}

.form-operation {
padding-left: 15px;

label {
font-weight: 400;
color: @filter-body-colour;
}
}
}

.facet-filter {
position: absolute;
height: 40%;
top: @filter-heading-height;
bottom: @filter-heading-height;
left: 0;
Expand All @@ -66,21 +93,26 @@
scrollbar-gutter: stable;
scrollbar-width: thin /* Firefox only*/;
padding-left: 1em;

.facet {
margin-bottom: 1em;
max-height: 600px;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;

h3 {
font-weight: 600;
margin-bottom: 0.25em;
}

ul {
padding:0;
margin:1px;
padding: 0;
margin: 1px;

li {
list-style: none;

.facet-count {
margin-left: 0.5em;
color: lighten(@filter-body-link-colour, 10%);
Expand All @@ -107,7 +139,9 @@
}
}
}

/* For webkit browsers only*/

.facet-filter::-webkit-scrollbar {
width: 8px;
}
Expand All @@ -125,9 +159,11 @@

&.map-mode {
@selected-color: @brand-success;

.facet-filter {
display: none;
}

.map-heading {
margin-top: @filter-heading-gap;
height: @filter-heading-height;
Expand All @@ -144,6 +180,7 @@
bottom: 0;
left: 0;
right: 0;

div.olControlZoom {
left: 2px;
top: 5px;
Expand Down
12 changes: 12 additions & 0 deletions web/less/style-eidc.less
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,15 @@ a#atlwdg-trigger {
display: none;
}
}

// To add a spatial search map to the search page, comment out these two styles
// -----------------------
.spatial,spatial-filter {
display: none;
}

.facet-filter {
position: absolute;
height: 90% !important;
}
// -----------------------
10 changes: 0 additions & 10 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion web/src/search/src/SearchApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default Backbone.Model.extend({
drawing: false,
mapsearch: false,
bbox: undefined,
op: undefined,
op: 'intersects',
facet: [],
term: undefined,
page: 1,
Expand Down Expand Up @@ -108,5 +108,19 @@ export default Backbone.Model.extend({
}
this.results = null
this.trigger('cleared:results')
},
/*
Update the search box, which triggers an update of the results
*/
setBbox (bbox) {
this.set({ bbox })
},

/*
Clear the search box, which triggers an update of the results
Also clear the 'op', since this is the spatial operation to perform, which is not needed if there is no bbox
*/
clearBbox () {
this.unset('bbox')
}
})
8 changes: 5 additions & 3 deletions web/src/search/src/SearchAppView.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import $ from 'jquery'
import Backbone from 'backbone'
import deparam from 'deparam.js'
import { FacetsPanelView, SearchFormView, SearchPageView } from './views'
import { FacetsPanelView, SearchFormView, SearchPageView, SpatialFilterView } from './views'

export default Backbone.View.extend({
el: '#search',
Expand Down Expand Up @@ -48,16 +48,18 @@ export default Backbone.View.extend({
model: this.model,
el: this.$('.search-form')
})

this.searchResultsView = new SearchPageView({
model: this.model,
el: this.$('.results')
})

this.facetsPanelView = new FacetsPanelView({
model: this.model,
el: this.$('.facet-filter')
})
this.spatialFilterView = new SpatialFilterView({
model: this.model,
el: this.$('.spatial-filter')
})
return this
}
})
18 changes: 18 additions & 0 deletions web/src/search/src/templates/spatialFilterTemplate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import _ from 'underscore'

export default _.template(`
<div class="map"></div>
<div class="form-operation">
<div>
<h3>Choose how to query records:</h3>
</div>
<div>
<input class="spatialOp" data-op="intersects" type="radio" name="operations" id="opIntersects" <% if (op === 'intersects') {%> checked<%}%>>
<label>Intersects</label>
</div>
<div>
<input class="spatialOp" data-op="iswithin" type="radio" name="operations" id="opWithin" <% if (op === 'iswithin') {%> checked<%}%>>
<label>Within</label>
</div>
</div>
`)
Loading

0 comments on commit bf34bde

Please sign in to comment.