diff --git a/docs/screenshots/views/maps/MapHelpPanel.png b/docs/screenshots/views/maps/MapHelpPanel.png new file mode 100644 index 000000000..7c277f4bb Binary files /dev/null and b/docs/screenshots/views/maps/MapHelpPanel.png differ diff --git a/src/css/map-view.css b/src/css/map-view.css index d3c9439e8..1812de6fe 100644 --- a/src/css/map-view.css +++ b/src/css/map-view.css @@ -126,7 +126,7 @@ /* ---- BADGE ---- */ -.map-view__badge{ +.map-view__badge { padding: 0.4em 0.5em 0.3em 0.55em; margin: 0 -0.2rem 0 0.3rem; font-size: 0.62rem; @@ -140,24 +140,25 @@ font-weight: 500; } -.map-view__badge--blue{ +.map-view__badge--blue { background-color: var(--map-col-blue); filter: none; } -.map-view__badge--green{ +.map-view__badge--green { background-color: var(--map-col-green); filter: none; } -.map-view__badge--yellow{ +.map-view__badge--yellow { background-color: var(--map-col-yellow); color: var(--map-col-bkg-lighter); filter: none; font-weight: 600; opacity: 0.9; } -.map-view__badge--contrast{ + +.map-view__badge--contrast { background-color: var(--map-col-text); color: var(--map-col-bkg); opacity: 0.8; @@ -612,23 +613,24 @@ represents 1 unit of the given distance measurement. */ border-radius: var(--map-border-radius); } -.layer-details__notification--blue{ +.layer-details__notification--blue { background-color: var(--map-col-blue); filter: none; } -.layer-details__notification--green{ +.layer-details__notification--green { background-color: var(--map-col-green); filter: none; } -.layer-details__notification--yellow{ +.layer-details__notification--yellow { background-color: var(--map-col-yellow); color: var(--map-col-bkg-lighter); filter: none; opacity: 0.9; } -.layer-details__notification--contrast{ + +.layer-details__notification--contrast { background-color: var(--map-col-text); color: var(--map-col-bkg); opacity: 0.95; @@ -972,3 +974,93 @@ other class: .ui-slider-range */ grid-auto-rows: min-content; grid-gap: 1rem; } + +/***************************************************************************************** + * + * Help panel + * + * Panel that shows navigation and other help information + * + */ + +.map-help-panel{ + width: 100%; +} + +.nav-help { + background-color: var(--map-col-bkg-lighter); + border-radius: var(--map-border-radius); +} + +.nav-help .map-view__button { + background-color: var(--map-col-bkg-lighter); + padding: 0.65rem 0.9rem; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + opacity: 0.8; +} + +.nav-help .map-view__button--active { + background-color: var(--map-col-bkg-lightest); + opacity: 1; +} + +.nav-help__img { + height: 48px; + width: 70px; +} + +.nav-help__instructions { + display: grid; + width: 100%; + gap: 0.8rem; + padding: 0.8rem 0.4rem; + background-color: var(--map-col-bkg-lightest); + border-radius: var(--map-border-radius); + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.nav-help__instructions.hidden { + display: none; +} + +.cesium-navigation-help-pan { + color: #66ccff; + font-weight: bold; +} + +.cesium-navigation-help-zoom { + color: #65fd00; + font-weight: bold; +} + +.cesium-navigation-help-rotate { + color: #ffd800; + font-weight: bold; +} + +.cesium-navigation-help-tilt { + color: #d800d8; + font-weight: bold; +} + +.nav-help__instruction { + display: grid; + grid-template-columns: 70px auto; + gap: 0.5rem; + align-items: center; +} + +.map-help-panel__title{ + text-transform: uppercase; + font-size: 0.95rem; + font-weight: 600; + letter-spacing: 0.06em; + margin: 0 0 0.8rem 0; + line-height: normal; +} + +.map-help-panel__section:not(:first-child) { + margin-top: 2.5rem; +} \ No newline at end of file diff --git a/src/js/models/maps/Map.js b/src/js/models/maps/Map.js index f15cfe4a5..b832d4a8f 100644 --- a/src/js/models/maps/Map.js +++ b/src/js/models/maps/Map.js @@ -42,8 +42,8 @@ define([ * side bar with layer list, etc. If true, the {@link MapView} will render * a {@link ToolbarView}. * @property {Boolean} [showLayerList=true] - Whether or not to show the - * layer list in the toolbar. If true, the {@link ToolbarView} will - * render a {@link LayerListView}. + * layer list in the toolbar. If true, the {@link ToolbarView} will render + * a {@link LayerListView}. * @property {Boolean} [showHomeButton=true] - Whether or not to show the * home button in the toolbar. * @property {Boolean} [toolbarOpen=false] - Whether or not the toolbar is @@ -56,6 +56,17 @@ define([ * users to click on map features to show more information about them. If * true, the {@link MapView} will render a {@link FeatureInfoView} and * will initialize "picking" in the {@link CesiumWidgetView}. + * @property {String} [clickFeatureAction="showDetails"] - The default + * action to take when a user clicks on a feature on the map. The + * available options are "showDetails" (show the feature details in the + * sidebar) or "zoom" (zoom to the feature's location). + * @property {Boolean} [showNavHelp=true] - Whether or not to show + * navigation instructions in the toolbar. + * @property {Boolean} [showFeedback=false] - Whether or not to show a + * feedback section in the toolbar with the text specified in + * feedbackText. + * @property {String} [feedbackText=null] - The text to show in the + * feedback section. showFeedback must be true for this to be shown. * * @example * { @@ -159,6 +170,12 @@ define([ * action to take when a user clicks on a feature on the map. The * available options are "showDetails" (show the feature details in the * sidebar) or "zoom" (zoom to the feature's location). + * @property {Boolean} [showNavHelp=true] - Whether or not to show + * navigation instructions in the toolbar. + * @property {Boolean} [showFeedback=false] - Whether or not to show a + * feedback section in the toolbar. + * @property {String} [feedbackText=null] - The text to show in the + * feedback section. */ defaults: function () { return { @@ -184,6 +201,9 @@ define([ showScaleBar: true, showFeatureInfo: true, clickFeatureAction: "showDetails", + showNavHelp: true, + showFeedback: false, + feedbackText: null }; }, diff --git a/src/js/templates/maps/cesium-nav-help.html b/src/js/templates/maps/cesium-nav-help.html new file mode 100644 index 000000000..34025b34e --- /dev/null +++ b/src/js/templates/maps/cesium-nav-help.html @@ -0,0 +1,107 @@ + diff --git a/src/js/views/maps/HelpPanelView.js b/src/js/views/maps/HelpPanelView.js new file mode 100644 index 000000000..8483958c0 --- /dev/null +++ b/src/js/views/maps/HelpPanelView.js @@ -0,0 +1,256 @@ +"use strict"; + +define(["backbone", "text!templates/maps/cesium-nav-help.html"], function ( + Backbone, + NavHelpTemplate +) { + /** + * @class MapHelpPanel + * @classdesc The MapHelpPanel view displays navigation instructions and other + * help information for the map. + * @classcategory Views/Maps + * @name MapHelpPanel + * @extends Backbone.View + * @screenshot views/maps/MapHelpPanel.png + * @since x.x.x + * @constructs MapHelpPanel + */ + var MapHelpPanel = Backbone.View.extend( + /** @lends MapHelpPanel.prototype */ { + /** + * The type of View this is + * @type {string} + */ + type: "MapHelpPanel", + + /** + * The HTML classes to use for this view's element + * @type {string} + */ + className: "map-help-panel", + + /** + * Initializes the MapHelpPanel + * @param {Object} options - A literal object with options to pass to the + * view + * @param {boolean} [options.showFeedback=true] - Set to false to hide the + * feedback section + * @param {boolean} [options.showNavHelp=true] - Set to false to hide the + * navigation instructions section + * @param {string} [options.feedbackText] - Text to show in the feedback + * section + */ + initialize: function (options) { + if (!options) options = {}; + + const validOptions = ["showFeedback", "feedbackText", "showNavHelp"]; + validOptions.forEach((option) => { + if (options.hasOwnProperty(option)) { + this[option] = options[option]; + } + }); + + }, + + /** + * The template to use to show the navigation instructions + * @type {function} + */ + navHelpTemplate: _.template(NavHelpTemplate), + + /** + * Set to false to hide the feedback section + * @type {boolean} + * @default true + */ + showFeedback: true, + + /** + * Set to false to hide the navigation instructions section + * @type {boolean} + * @default true + */ + showNavHelp: true, + + /** + * Text to show in the feedback section + * @type {string} + */ + feedbackText: `Please contact the administrator of this map for help.`, + + /** + * The sections to show in the help panel + * @type {Object[]} + * @property {string} id - The id of the section + * @property {string} title - The title of the section + * @property {string} render - The name of the method to call to render + * the section. The method will be passed the container within which to + * render the section. It should return the container element with the + * section added. Methods will be called with the view as the context. + */ + sections: [ + { + id: "nav-help", + title: "Navigation Instructions", + render: "renderNavHelp", + }, + { + id: "feedback", + title: "Feedback", + render: "renderFeedback", + }, + ], + + /** + * Renders the MapHelpPanel + * @returns {MapHelpPanel} Returns the view + */ + render: function () { + const view = this; + + let sections = JSON.parse(JSON.stringify(view.sections)); + + if (view.showFeedback === false) { + sections = sections.filter((section) => { + return section.id !== "feedback"; + }); + } + + if (view.showNavHelp === false) { + sections = sections.filter((section) => { + return section.id !== "nav-help"; + }); + } + + sections.forEach((section) => { + view.renderSection(section); + }); + + return view; + }, + + /** + * Renders a section of the help panel + * @param {Object} section - The options for the section, see + * {@link MapHelpPanel#sections} + */ + renderSection: function (section) { + try { + const view = this; + const renderMethod = view[section.render] || null; + if (!renderMethod) return; + + const contentContainerClass = "map-help-panel__content"; + + const sectionEl = document.createElement("section"); + sectionEl.classList.add("map-help-panel__section"); + sectionEl.innerHTML = `

${section.title}

+
`; + const contentEl = sectionEl.querySelector( + "." + contentContainerClass + ); + renderMethod.call(view, contentEl); + + view.el.appendChild(sectionEl); + + return sectionEl; + } catch (e) { + console.log("Error rendering a help panel section", e); + } + }, + + /** + * Renders the navigation instructions + * @param {HTMLElement} containerEl - The element to render the navigation + * instructions within + * @returns {HTMLElement} Returns the container element with the + * navigation instructions added + */ + renderNavHelp: function (containerEl) { + const view = this; + const cid = this.cid; + const cesiumUrl = + "https://cesium.com/downloads/cesiumjs/releases/1.91/Build/Cesium/"; + + const mouseButtonId = "nav-help-mouse-" + cid; + const touchButtonId = "nav-help-touch-" + cid; + const mouseSectionId = "nav-help-mouse-section-" + cid; + const touchSectionId = "nav-help-touch-section-" + cid; + + // Create the HTML and add it to the container + const navHelpHTML = this.navHelpTemplate({ + mouseButtonId, + touchButtonId, + mouseSectionId, + touchSectionId, + cesiumUrl, + }); + containerEl.innerHTML = navHelpHTML; + + // Select the elements + const mouseButtonEl = containerEl.querySelector("#" + mouseButtonId); + const touchButtonEl = containerEl.querySelector("#" + touchButtonId); + const mouseSectionEl = containerEl.querySelector("#" + mouseSectionId); + const touchSectionEl = containerEl.querySelector("#" + touchSectionId); + + // Add listeners to the buttons to toggle the sections + mouseButtonEl.addEventListener("click", () => { + view.showSection(mouseSectionEl, mouseButtonEl); + view.hideSection(touchSectionEl, touchButtonEl); + }); + touchButtonEl.addEventListener("click", () => { + view.showSection(touchSectionEl, touchButtonEl); + view.hideSection(mouseSectionEl, mouseButtonEl); + }); + + // Show only the mouse section by default + view.hideSection(touchSectionEl, touchButtonEl); + view.showSection(mouseSectionEl, mouseButtonEl); + + return containerEl; + }, + + /** + * Renders the feedback section + * @param {HTMLElement} containerEl - The element to render the feedback + * section within + * @returns {HTMLElement} Returns the container element with the feedback + * section added + */ + renderFeedback: function (containerEl) { + containerEl.innerHTML = this.feedbackText; + return containerEl; + }, + + /** + * Hides a section by adding the hidden class, and removes the active + * class from the button if one is provided + * @param {HTMLElement} sectionEl - The section element to hide + * @param {HTMLElement} [buttonEl] - The button element to remove the + * active class from + */ + hideSection: function (sectionEl, buttonEl) { + if (!sectionEl) return; + sectionEl.classList.add("hidden"); + if (!buttonEl) return; + buttonEl.classList.remove("map-view__button--active"); + }, + + /** + * Shows a section by removing the hidden class, and adds the active class + * to the button if one is provided + * @param {HTMLElement} sectionEl - The section element to show + * @param {HTMLElement} [buttonEl] - The button element to add the active + * class to + */ + showSection: function (sectionEl, buttonEl) { + if (!sectionEl) return; + sectionEl.classList.remove("hidden"); + if (!buttonEl) return; + buttonEl.classList.add("map-view__button--active"); + }, + } + ); + + return MapHelpPanel; +}); diff --git a/src/js/views/maps/ToolbarView.js b/src/js/views/maps/ToolbarView.js index e8eac5e69..b9b8aa320 100644 --- a/src/js/views/maps/ToolbarView.js +++ b/src/js/views/maps/ToolbarView.js @@ -9,7 +9,8 @@ define( 'models/maps/Map', // Sub-views - TODO: import these as needed 'views/maps/LayerListView', - 'views/maps/DrawToolView' + 'views/maps/DrawToolView', + 'views/maps/HelpPanelView' ], function ( $, @@ -19,7 +20,8 @@ define( Map, // Sub-views LayerListView, - DrawTool + DrawTool, + HelpPanel ) { /** @@ -157,7 +159,7 @@ define( * * @type {SectionOption[]} */ - sections: [ + sectionOptions: [ { label: 'Layers', icon: '', @@ -179,6 +181,16 @@ define( icon: 'pencil', view: DrawTool, viewOptions: {} + }, + { + label: 'Help', + icon: 'question-sign', + view: HelpPanel, + viewOptions: { + showFeedback: 'model.showFeedback', + feedbackText: 'model.feedbackText', + showNavHelp: 'model.showNavHelp', + } } ], @@ -205,9 +217,16 @@ define( if (!this.model || !(this.model instanceof Map)) { this.model = new Map(); } + if(this.model.get('toolbarOpen') === true) { this.isOpen = true; } + + + // Deep clone the section options so that the original array is not + // modified + this.sections = _.map(this.sectionOptions, _.clone); + if (this.model.get("showLayerList") === false) { this.sections = this.sections.filter( (section) => section.label !== "Layers" @@ -218,8 +237,13 @@ define( (section) => section.label !== "Home" ); } + if (!this.model.get("showNavHelp") && !this.model.get("showFeedback")) { + this.sections = this.sections.filter( + (section) => section.label !== "Help" + ); + } } catch (e) { - console.log('A ToolbarView failed to initialize. Error message: ' + e); + console.log('Error initializing a ToolbarView', e); } },