Skip to content

Commit

Permalink
Support flying to a user-entered latitude, longitude pair
Browse files Browse the repository at this point in the history
fixes #2246
  • Loading branch information
ianguerin committed Feb 2, 2024
1 parent ac48bea commit c60a63b
Show file tree
Hide file tree
Showing 8 changed files with 493 additions and 10 deletions.
Binary file added docs/screenshots/views/maps/ViewFinderView.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions src/css/map-view.css
Original file line number Diff line number Diff line change
Expand Up @@ -1068,4 +1068,47 @@ other class: .ui-slider-range */

.map-help-panel__section:not(:first-child) {
margin-top: 2.5rem;
}

.view-finder__field {
border-radius: 4px;
border: 1px solid var(--portal-col-bkg-active);
display: flex;
flex-direction: row;
background: var(--portal-col-bkg-lighter);

&:focus-within {
border-color: rgba(82, 168, 236, .8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(82, 168, 236, .6);
}

.view-finder__input {
border: none;
box-sizing: border-box;
flex: 1;
height: 100%;
margin: 0;

&:focus {
border: none;
box-shadow: none;
}
}

.view-finder__button {
border: none;
border-radius: 4px;
color: var(--portal-col-buttons);
background: none;

&:hover {
color: #ffffff;
}
}
}

.view-finder__error {
color: var(--map-col-text);
font-size: .8rem;
padding: 4px 12px;
}
19 changes: 19 additions & 0 deletions src/js/templates/maps/view-finder.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<div class="<%= classNames.baseClass %>">
<h2>View finder</h2>
<span>Search for a latitude and longitude pair and click the "search" button to show that point on the map.</span>
<br /><br />
<div class="view-finder__form-field">
<div class="view-finder__field">
<input class="<%= classNames.input %>" type="text" placeholder="<%= placeholder %>" value="<%= inputValue %>" />
<button class="<%= classNames.button %>">
<i class="icon icon-search"></i>
</button>
</div>

<% if(errorMessage) { %>
<div class="view-finder__error">
<%= errorMessage %>
</div>
<% } %>
</div>
</div>
44 changes: 34 additions & 10 deletions src/js/views/maps/ToolbarView.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ define(
// Sub-views - TODO: import these as needed
'views/maps/LayerListView',
'views/maps/DrawToolView',
'views/maps/HelpPanelView'
'views/maps/HelpPanelView',
'views/maps/ViewFinderView',
],
function (
$,
Expand All @@ -21,7 +22,8 @@ define(
// Sub-views
LayerListView,
DrawTool,
HelpPanel
HelpPanel,
ViewFinderView,
) {

/**
Expand Down Expand Up @@ -183,6 +185,16 @@ define(
// view: DrawTool,
// viewOptions: {}
// },
{
label: 'View finder',
icon: 'search',
view: ViewFinderView,
action(view, model) {
const sectionEl = this;
view.defaultActivationAction(sectionEl);
sectionEl.sectionView.focusInput();
},
},
{
label: 'Help',
icon: 'question-sign',
Expand Down Expand Up @@ -219,7 +231,7 @@ define(
this.model = new Map();
}

if(this.model.get('toolbarOpen') === true) {
if (this.model.get('toolbarOpen') === true) {
this.isOpen = true;
}

Expand Down Expand Up @@ -286,14 +298,17 @@ define(
var linkEl = view.renderSectionLink(sectionOption)
var action = sectionOption.action
let contentEl = null;
let sectionView;
if (sectionOption.view) {
contentEl = view.renderSectionContent(sectionOption)
const { contentContainer, sectionContent } = view.renderSectionContent(sectionOption)
contentEl = contentContainer;
sectionView = sectionContent;
}
// Set the section to false to start
var isActive = false
// Save a reference to these elements and their status. sectionEl is an
// object that has type SectionElement (documented in comments below)
var sectionEl = { linkEl, contentEl, isActive, action }
var sectionEl = { linkEl, contentEl, isActive, action, sectionView };
view.sectionElements.push(sectionEl)
// Attach the link and content to the view
if (contentEl) {
Expand Down Expand Up @@ -336,6 +351,7 @@ define(
* section's content, and open/close the toolbar.
* @property {Boolean} isActive True if this is the active section, false
* otherwise.
* @property {Backbone.View} sectionView The associated Backbone.View instance.
*/

/**
Expand Down Expand Up @@ -481,7 +497,7 @@ define(
var sectionContent = new sectionOption.view(viewOptions)
contentContainer.appendChild(sectionContent.el)
sectionContent.render()
return contentContainer
return { contentContainer, sectionContent }
}
catch (error) {
console.log('Error rendering ToolbarView section', error);
Expand Down Expand Up @@ -527,23 +543,31 @@ define(
* @param {SectionElement} sectionEl The section to activate
*/
activateSection: function (sectionEl) {
if(!sectionEl) return;
if (!sectionEl) return;
try {
if (sectionEl.action && typeof sectionEl.action === 'function') {
const view = this;
const model = this.model;
sectionEl.action(view, model)
} else {
sectionEl.isActive = true;
sectionEl.contentEl.classList.add(this.classes.contentActive)
sectionEl.linkEl.classList.add(this.classes.linkActive)
this.defaultActivationAction(sectionEl);
}
}
catch (error) {
console.log('Failed to show a section in a ToolbarView', error);
}
},

/**
* The default action for a section being activated.
* @param {SectionElement} sectionEl The section to activate
*/
defaultActivationAction(sectionEl) {
sectionEl.isActive = true;
sectionEl.contentEl.classList.add(this.classes.contentActive)
sectionEl.linkEl.classList.add(this.classes.linkActive)
},

/**
* Hide the content of a section
* @param {SectionElement} sectionEl The section to inactivate
Expand Down
155 changes: 155 additions & 0 deletions src/js/views/maps/ViewFinderView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"use strict";

define([
"backbone",
"text!templates/maps/view-finder.html",
], (
Backbone,
Template,
) => {
/**
* @class ViewFinderView
* @classdesc The ViewFinderView allows a user to search for a latitude and longitude in the map view.
* @classcategory Views/Maps
* @name ViewFinderView
* @extends Backbone.View
* @screenshot views/maps/ViewFinderView.png
* @since 2.27.1
* @constructs ViewFinderView
*/
var ViewFinderView = Backbone.View.extend({
/**
* The type of View this is
* @type {string}
*/
type: "ViewFinderView",

/**
* The HTML classes to use for this view's element
* @type {string}
*/
className: classNames.baseClass,

/**
* The events this view will listen to and the associated function to call.
* @type {Object}
*/
events: {
[`click .${classNames.button}`]: 'search',
[`keyup .${classNames.input}`]: 'keyup',
},

/**
* Values meant to be used by the rendered HTML template.
*/
templateVars: {
errorMessage: "",
inputValue: "",
placeholder: "Search by latitude and longitude",
classNames,
},

/**
* @typedef {Object} ViewFinderViewOptions
* @property {Map} The Map model associated with this view allowing control
* of panning to different locations on the map.
*/
initialize(options) {
this.mapModel = options.model;
},

render() {
this.focusInput();

this.el.innerHTML = _.template(Template)(this.templateVars);
},

/**
* Focus the input field on a delay to allow for the input to be
* visible on the page before attempting to focus.
*/
focusInput() {
_.defer(() => {
const input = this.getInput();
input.focus();
// Move cursor to end of input.
input.val("");
input.val(this.templateVars.inputValue);
});
},

getInput() {
return this.$el.find(`.${classNames.input}`);
},

getButton() {
return this.$el.find(`.${classNames.button}`);
},

/** Event handler for Backbone.View configuration. */
keyup(event) {
this.templateVars.inputValue = this.getInput().val();
if (event.key === "Enter") {
this.search();
}
},

/** Event handler for Backbone.View configuration. */
search() {
this.clearError();

const coords = this.parseValue(this.templateVars.inputValue)
if (!coords) return;

this.model.zoomTo({ ...coords, height: 321321 /* meters */ });
},

/**
* Parse the user's input as a pair of floating point numbers. Log errors to the UI
* @return {{Number,Number}|undefined} Undefined represents an irrecoverable user input,
* otherwise returns a latitude, longitude pair.
*/
parseValue(value) {
const matches = value.match(floatsRegex);
const hasBannedChars = value.match(bannedCharactersRegex) != null;
if (matches?.length !== 2 || isNaN(matches[0]) || isNaN(matches[1]) || hasBannedChars) {
this.setError("Try entering a search query with two numerical values representing a latitude and longitude (e.g. 64.84, -147.72).");
return;
}

const latitude = Number(matches[0]);
const longitude = Number(matches[1]);
if (latitude > 90 || latitude < -90) {
this.setError("Latitude values outside of the range of -90 to 90 may behave unexpectedly.");
} else if (longitude > 180 || longitude < -180) {
this.setError("Longitude values outside of the range of -180 to 180 may behave unexpectedly.");
}

return { latitude, longitude };
},

clearError() {
this.setError("");
},

setError(errorMessage) {
this.templateVars.errorMessage = errorMessage;
this.render();
},
});

return ViewFinderView;
});

// Regular expression matching a string that contains two numbers optionally separated by a comma.
const floatsRegex = /[+-]?[0-9]*[.]?[0-9]+/g;

// Regular expression matching everything except numbers, periods, and commas.
const bannedCharactersRegex = /[^0-9,.+-\s]/g;

// Class names that correspond to elements in the template.
const classNames = {
baseClass: 'view-finder',
button: "view-finder__button",
input: "view-finder__input",
};
1 change: 1 addition & 0 deletions test/config/tests.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"unit": [
"./js/specs/unit/views/maps/ViewFinderView.spec.js",
"./js/specs/unit/collections/SolrResults.spec.js",
"./js/specs/unit/models/Search.spec.js",
"./js/specs/unit/models/filters/Filter.spec.js",
Expand Down
Loading

0 comments on commit c60a63b

Please sign in to comment.