From 21c6d6a6c658f5c1703535c908fa7514bec4cbbf Mon Sep 17 00:00:00 2001 From: goldpbear Date: Wed, 8 Nov 2017 10:49:27 +0100 Subject: [PATCH] feat(nyrp) add full-height layer panel overlay --- src/flavors/nyrp/config.yml | 138 ++++--- src/flavors/nyrp/jstemplates/sidebar.html | 77 ++++ src/flavors/nyrp/static/css/custom.css | 88 ++++ src/flavors/nyrp/static/js/views/app-view.js | 388 ++++++++++++++++++ .../nyrp/static/js/views/gis-legend-view.js | 63 +++ .../nyrp/static/js/views/sidebar-view.js | 25 ++ 6 files changed, 727 insertions(+), 52 deletions(-) create mode 100644 src/flavors/nyrp/jstemplates/sidebar.html create mode 100644 src/flavors/nyrp/static/js/views/app-view.js create mode 100644 src/flavors/nyrp/static/js/views/gis-legend-view.js create mode 100644 src/flavors/nyrp/static/js/views/sidebar-view.js diff --git a/src/flavors/nyrp/config.yml b/src/flavors/nyrp/config.yml index 749d670f84..9aafafbc4d 100644 --- a/src/flavors/nyrp/config.yml +++ b/src/flavors/nyrp/config.yml @@ -53,14 +53,15 @@ map: url: https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png attribution: Terms and conditions © OpenStreetMap contributors, CC-BY-SA. Mapbox. Geocoding Courtesy of MapQuest .' + - id: terrain + type: basemap + url: //stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png + maxZoom: 16 + - id: satellite url: //api.tiles.mapbox.com/v4/smartercleanup.pe3o4gdn/{z}/{x}/{y}.png?access_token=pk.eyJ1Ijoic21hcnRlcmNsZWFudXAiLCJhIjoiTnFhUWc2cyJ9.CqPJH-9yspIMudowQJx2Uw attribution: '© OpenStreetMap contributors, CC-BY-SA. Terms & Feedback. Geocoding Courtesy of MapQuest .' - - id: dark - url: //api.mapbox.com/styles/v1/smartercleanup/cis9wdf5h003w2xt6atlz48yh/tiles/256/{z}/{x}/{y}?access_token=pk.eyJ1Ijoic21hcnRlcmNsZWFudXAiLCJhIjoiTnFhUWc2cyJ9.CqPJH-9yspIMudowQJx2Uw - attribution: 'Hello World' - - id: nyrp url: https://dev-api.heyduwamish.org/api/v2/smartercleanup/datasets/nyrp type: place @@ -71,6 +72,51 @@ map: type: place slug: featured + # TODO: add real layer + - id: transit + type: wms + url: https://geoserver.mapseed.org/geoserver/kc_sewers/wms? + version: 1.3.0 + format: image/png + transparent: true + layers: sewer + + # TODO: add real layer + - id: biking + type: wms + url: https://geoserver.mapseed.org/geoserver/kc_sewers/wms? + version: 1.3.0 + format: image/png + transparent: true + layers: sewer + + # TODO: add real layer + - id: community + type: wms + url: https://geoserver.mapseed.org/geoserver/kc_sewers/wms? + version: 1.3.0 + format: image/png + transparent: true + layers: sewer + + # TODO: add real layer + - id: heat-waves + type: wms + url: https://geoserver.mapseed.org/geoserver/kc_sewers/wms? + version: 1.3.0 + format: image/png + transparent: true + layers: sewer + + # TODO: add real layer + - id: flood-zones + type: wms + url: https://geoserver.mapseed.org/geoserver/kc_sewers/wms? + version: 1.3.0 + format: image/png + transparent: true + layers: sewer + # The values map the place type to map icons (defined below). place_types: observation: @@ -210,67 +256,55 @@ place_types: iconSize: [30, 30] iconAnchor: [15, 15] - -# Sidebar and activity stream should be enabled and disabled together! -# note: sidebar is aka 'MasterLegend' -# TODO: Couple the sidebar and activity stream because the activity stream is a component of the sidebar sidebar: enabled: true - legend_enabled: true - slug: filter-type - # Layers are defined in the "map config" section - # A panel's icon is a icon's name taken from here: - # https://fortawesome.github.io/Font-Awesome/icons/ panels: - id: gis-layers - icon: list - view: GISLegendView - title: _(Map Layers:) basemaps: - id: osm - title: _(OpenStreetMap) + icon: map-o + title: _(Default) visibleDefault: true - id: satellite - title: _(Satellite View) + icon: map + title: _(Satellite) visibleDefault: false - - id: dark - title: _(Dark) + - id: terrain + icon: globe + title: _(Terrain) visibleDefault: false groupings: - - id: grp-community - title: _(Community Data) - visibleDefault: true + - id: map-detail + title: _(Map detail) layers: + - id: transit + icon: train + title: _(Transit) + visibleDefault: false + - id: biking + icon: bicycle + title: _(Biking) + visibleDefault: false - id: nyrp - title: _(Community Reports) - visibleDefault: true - - id: nyrpfeatured - title: _(Featured Places) - visibleDefault: true - - - id: ticker - view: ActivityView - icon: comments-o - activity: true - title: _(Activity Stream:) - - - id: reports_legend - title: _(Citizen Reports:) - view: LegendView - icon: info-circle - items: - - title: _(Observations) - image: /static/css/images/markers/marker-observation.png - url: /filter/observation - - title: _(Ideas) - image: /static/css/images/markers/marker-idea.png - url: /filter/idea - - title: _(Questions) - image: /static/css/images/markers/marker-question.png - url: /filter/question - - title: _(Complaints) - image: /static/css/images/markers/marker-complaint.png - url: /filter/complaint + icon: map-marker + title: _(Comments) + visibleDefault: false + - id: community + icon: users + title: _(Community) + visibleDefault: false + - id: climate-risks + title: _(Map detail) + layers: + - id: heat-waves + icon: thermometer-full + title: _(Heat Waves) + visibleDefault: false + - id: flood-zones + icon: shower + title: _(Flood Zones) + visibleDefault: false + activity: # Optional. Activity is supported by default. Set to false to disable. diff --git a/src/flavors/nyrp/jstemplates/sidebar.html b/src/flavors/nyrp/jstemplates/sidebar.html new file mode 100644 index 0000000000..9d171a9c84 --- /dev/null +++ b/src/flavors/nyrp/jstemplates/sidebar.html @@ -0,0 +1,77 @@ + diff --git a/src/flavors/nyrp/static/css/custom.css b/src/flavors/nyrp/static/css/custom.css index 4374ac7ccf..b9513e846d 100644 --- a/src/flavors/nyrp/static/css/custom.css +++ b/src/flavors/nyrp/static/css/custom.css @@ -4,6 +4,94 @@ * NOTE: "With great power comes great responsibility." */ +#sidebar-panel { + position: absolute; + opacity: 0.9; + left: 0; + top: 0; + background-color: #FFFFFF; + height: 100%; + width: 250px; + max-width: 250px; + z-index: 5000; + overflow: scroll; + -webkit-box-shadow: 2px 0px 1px 0px rgba(50, 50, 50, 0.3); + -moz-box-shadow: 2px 0px 1px 0px rgba(50, 50, 50, 0.3); + box-shadow: 2px 0px 1px 0px rgba(50, 50, 50, 0.3); +} + +.sidebar-panel--hidden { + display: none; +} + +.sidebar-panel--visible { + display: block; +} + +#sidebar-panel hr { + margin-top: 0; + margin-bottom: 0; +} + +.sidebar-panel__close-panel:after { + font-family: FontAwesome; + content: "\f00d"; + float: right; + padding-top: 10px; + padding-right: 10px; +} + +.sidebar-panel__close-panel:hover { + cursor: pointer; + color: #888888; +} + +.layer-panel-icon { + float: left; + padding-right: 8px; + width: 1.2em; + text-align: center; +} + +.layer-type-grouping { + margin-left: 20px; +} + +.layer-type-grouping-title { + margin-bottom: 10px; + margin-top: 10px; +} + +.layer-type-li { + height: 2.0em; +} + +.layer-type-title label { + margin: 0; + border-bottom: 2px solid transparent; +} + +.layer-type-title input:checked ~ label { + background-color: transparent; + border-bottom: 2px solid rgba(0,0,255,1); +} + +.layer-type-title input:hover ~ label { + background-color: transparent; + border-bottom: 2px solid rgba(0,0,255,0.5); +} + +.show-layer-panel:before { + font-family: FontAwesome; + content: "\f03a"; + font-size: 1.4em; +} + +@media only screen and (min-width: 60em) { + #main-btns-container.main-btns-container--offset-left { + left: 260px; + } +} /* =Header -------------------------------------------------------------- */ diff --git a/src/flavors/nyrp/static/js/views/app-view.js b/src/flavors/nyrp/static/js/views/app-view.js new file mode 100644 index 0000000000..ee35e453dc --- /dev/null +++ b/src/flavors/nyrp/static/js/views/app-view.js @@ -0,0 +1,388 @@ +const AppView = require("../../../../../base/static/js/views/app-view.js"); +const GeocodeAddressView = require('../../../../../base/static/js/views/geocode-address-view'); +const PlaceCounterView = require('../../../../../base/static/js/views/place-counter-view'); +const PagesNavView = require('../../../../../base/static/js/views/pages-nav-view'); +const AuthNavView = require('../../../../../base/static/js/views/auth-nav-view'); +const MapView = require('../../../../../base/static/js/views/map-view'); +const SidebarView = require('../../../../../flavors/nyrp/static/js/views/sidebar-view'); +const ActivityView = require('../../../../../base/static/js/views/activity-view'); +const PlaceListView = require('../../../../../base/static/js/views/place-list-view'); +const RightSidebarView = require('../../../../../base/static/js/views/right-sidebar-view'); + +module.exports = AppView.extend({ + events: { + 'click #add-place': 'onClickAddPlaceBtn', + 'click .close-btn': 'onClickClosePanelBtn', + 'click .collapse-btn': 'onToggleSidebarVisibility', + 'click .list-toggle-btn': 'toggleListView', + // BEGIN FLAVOR-SPECIFIC CODE + 'click .show-layer-panel': 'toggleLayerPanel' + // END FLAVOR-SPECIFIC CODE + }, + initialize: function() { + + // store promises returned from collection fetches + Shareabouts.deferredCollections = []; + + var self = this, + // Only include submissions if the list view is enabled (anything but false) + includeSubmissions = this.options.appConfig.list_enabled !== false, + placeParams = { + // NOTE: this is to simply support the list view. It won't + // scale well, so let's think about a better solution. + include_submissions: includeSubmissions + }; + + // Use the page size as dictated by the server by default, unless + // directed to do otherwise in the configuration. + if (this.options.appConfig.places_page_size) { + placeParams.page_size = this.options.appConfig.places_page_size; + } + + // Bootstrapped data from the page + this.activities = this.options.activities; + this.places = this.options.places; + this.landmarks = this.options.landmarks; + + // Caches of the views (one per place) + this.placeFormView = null; + this.placeDetailViews = {}; + this.landmarkDetailViews = {}; + this.activeDetailView; + + // this flag is used to distinguish between user-initiated zooms and + // zooms initiated by a leaflet method + this.isProgrammaticZoom = false; + this.isStoryActive = false; + + $("body").ajaxError(function(evt, request, settings) { + $("#ajax-error-msg").show(); + }); + + $("body").ajaxSuccess(function(evt, request, settings) { + $("#ajax-error-msg").hide(); + }); + + if (this.options.activityConfig.show_in_right_panel === true) { + $("body").addClass("right-sidebar-visible"); + $("#right-sidebar-container").html( + "", + ); + } + + $(document).on("click", ".activity-item a", function(evt) { + window.app.clearLocationTypeFilter(); + }); + + // Globally capture clicks. If they are internal and not in the pass + // through list, route them through Backbone's navigate method. + $(document).on("click", 'a[href^="/"]', function(evt) { + var $link = $(evt.currentTarget), + href = $link.attr("href"), + url, + isLinkToPlace = false; + + _.each(self.options.datasetConfigs.places, function(dataset) { + if (href.indexOf("/" + dataset.slug) === 0) isLinkToPlace = true; + }); + + // Allow shift+click for new tabs, etc. + if ( + ($link.attr("rel") === "internal" || + href === "/" || + isLinkToPlace || + href.indexOf("/filter") === 0) && + !evt.altKey && + !evt.ctrlKey && + !evt.metaKey && + !evt.shiftKey + ) { + evt.preventDefault(); + + // Remove leading slashes and hash bangs (backward compatablility) + url = href.replace(/^\//, "").replace("#!/", ""); + + // # Instruct Backbone to trigger routing events + self.options.router.navigate(url, { + trigger: true, + }); + + return false; + } + }); + + // On any route (/place or /page), hide the list view + this.options.router.bind( + "route", + function(route) { + if ( + !_.contains(this.getListRoutes(), route) && + this.listView && + this.listView.isVisible() + ) { + this.hideListView(); + } + }, + this, + ); + + // Only append the tools to add places (if supported) + $("#map-container").append(Handlebars.templates["centerpoint"]()); + // NOTE: append add place/story buttons after the #map-container + // div (rather than inside of it) in order to support bottom-clinging buttons + $("#map-container").after( + Handlebars.templates["add-places"](this.options.placeConfig), + ); + + this.pagesNavView = new PagesNavView({ + el: "#pages-nav-container", + pagesConfig: this.options.pagesConfig, + placeConfig: this.options.placeConfig, + router: this.options.router, + }).render(); + + this.authNavView = new AuthNavView({ + el: "#auth-nav-container", + apiRoot: this.options.apiRoot, + router: this.options.router, + }).render(); + + this.basemapConfigs = _.find(this.options.sidebarConfig.panels, function( + panel, + ) { + return "basemaps" in panel; + }).basemaps; + // Init the map view to display the places + this.mapView = new MapView({ + el: "#map", + mapConfig: this.options.mapConfig, + sidebarConfig: this.options.sidebarConfig, + basemapConfigs: this.basemapConfigs, + legend_enabled: !!this.options.sidebarConfig.legend_enabled, + places: this.places, + landmarks: this.landmarks, + router: this.options.router, + placeTypes: this.options.placeTypes, + cluster: this.options.cluster, + placeDetailViews: this.placeDetailViews, + placeConfig: this.options.placeConfig + }); + + if (self.options.sidebarConfig.enabled) { + new SidebarView({ + el: "#sidebar-container", + mapView: this.mapView, + sidebarConfig: this.options.sidebarConfig, + placeConfig: this.options.placeConfig + }).render(); + + // BEGIN FLAVOR-SPECIFIC CODE + this.$(".leaflet-top.leaflet-right").append( + '
' + + '' + + "
", + ); + // END FLAVOR-SPECIFIC CODE + } + + // Activity is enabled by default (undefined) or by enabling it + // explicitly. Set it to a falsey value to disable activity. + if ( + _.isUndefined(this.options.activityConfig.enabled) || + this.options.activityConfig.enabled + ) { + // Init the view for displaying user activity + this.activityView = new ActivityView({ + el: "ul.recent-points", + activities: this.activities, + places: this.places, + landmarks: this.landmarks, + placeConfig: this.options.placeConfig, + router: this.options.router, + placeTypes: this.options.placeTypes, + surveyConfig: this.options.surveyConfig, + supportConfig: this.options.supportConfig, + placeConfig: this.options.placeConfig, + mapConfig: this.options.mapConfig, + // How often to check for new content + interval: this.options.activityConfig.interval || 30000, + }); + } + + // Init the address search bar + this.geocodeAddressView = new GeocodeAddressView({ + el: "#geocode-address-bar", + router: this.options.router, + mapConfig: this.options.mapConfig, + }).render(); + + // Init the place-counter + this.placeCounterView = new PlaceCounterView({ + el: "#place-counter", + router: this.options.router, + mapConfig: this.options.mapConfig, + places: this.places, + }).render(); + + // When the user chooses a geocoded address, the address view will fire + // a geocode event on the namespace. At that point we center the map on + // the geocoded location. + $(Shareabouts).on("geocode", function(evt, locationData) { + self.mapView.zoomInOn(locationData.latLng); + + if (self.isAddingPlace()) { + self.placeFormView.setLatLng(locationData.latLng); + // Don't pass location data into our geolocation's form field + // self.placeFormView.setLocation(locationData); + } + }); + + // When the map center moves, the map view will fire a mapmoveend event + // on the namespace. If the move was the result of the user dragging, a + // mapdragend event will be fired. + // + // If the user is adding a place, we want to take the opportunity to + // reverse geocode the center of the map, if geocoding is enabled. If + // the user is doing anything else, we just want to clear out any text + // that's currently set in the address search bar. + $(Shareabouts).on("mapdragend", function(evt) { + if (self.isAddingPlace()) { + self.conditionallyReverseGeocode(); + } else if (self.geocodeAddressView) { + self.geocodeAddressView.setAddress(""); + } + }); + + // After reverse geocoding, the map view will fire a reversegeocode + // event. This should only happen when adding a place while geocoding + // is enabled. + $(Shareabouts).on("reversegeocode", function(evt, locationData) { + var locationString = Handlebars.templates["location-string"]( + locationData, + ); + self.geocodeAddressView.setAddress($.trim(locationString)); + self.placeFormView.geocodeAddressPlaceView.setAddress( + $.trim(locationString), + ); + self.placeFormView.setLatLng(locationData.latLng); + // Don't pass location data into our geolocation's form field + // self.placeFormView.setLocation(locationData); + }); + + // List view is enabled by default (undefined) or by enabling it + // explicitly. Set it to a falsey value to disable activity. + if ( + _.isUndefined(this.options.appConfig.list_enabled) || + this.options.appConfig.list_enabled + ) { + this.listView = new PlaceListView({ + el: "#list-container", + placeCollections: self.places, + placeConfig: this.options.placeConfig, + }).render(); + } + + // Cache panel elements that we use a lot + this.$panel = $("#content"); + this.$panelContent = $("#content article"); + this.$panelCloseBtn = $(".close-btn"); + this.$centerpoint = $("#centerpoint"); + this.$addButton = $("#add-place-btn-container"); + + // Bind to map move events so we can style our center points + // with utmost awesomeness. + this.mapView.map.on("zoomend", this.onMapZoomEnd, this); + this.mapView.map.on("movestart", this.onMapMoveStart, this); + this.mapView.map.on("moveend", this.onMapMoveEnd, this); + // For knowing if the user has moved the map after opening the form. + this.mapView.map.on("dragend", this.onMapDragEnd, this); + + // If report stories are enabled, build the data structure + // we need to enable story navigation + _.each(this.options.storyConfig, function(story) { + var storyStructure = {}, + totalStoryElements = story.order.length; + _.each(story.order, function(config, i) { + storyStructure[config.url] = { + zoom: config.zoom || story.default_zoom, + hasCustomZoom: config.zoom ? true : false, + panTo: config.panTo || null, + visibleLayers: config.visible_layers || story.default_visible_layers, + previous: + story.order[(i - 1 + totalStoryElements) % totalStoryElements].url, + next: story.order[(i + 1) % totalStoryElements].url, + basemap: config.basemap || story.default_basemap, + spotlight: config.spotlight === false ? false : true, + sidebarIconUrl: config.sidebar_icon_url, + }; + }); + story.order = storyStructure; + }); + + // This is the "center" when the popup is open + this.offsetRatio = { x: 0.2, y: 0.0 }; + + _.each(this.places, function(value, key) { + self.placeDetailViews[key] = {}; + }); + + _.each(this.landmarks, function(value, key) { + self.landmarkDetailViews[key] = {}; + }); + + // Show tools for adding data + this.setBodyClass(); + this.showCenterPoint(); + + // Load places from the API + this.loadPlaces(placeParams); + + // Load landmarks from the API + this.loadLandmarks(); + + // Load activities from the API + _.each(this.activities, function(collection, key) { + collection.fetch({ + reset: true, + attribute: "target", + attributesToAdd: { + datasetId: _.find(self.options.mapConfig.layers, function(layer) { + return layer.id == key; + }).id, + datasetSlug: _.find(self.options.mapConfig.layers, function(layer) { + return layer.id == key; + }).slug, + }, + }); + }); + + if (this.options.rightSidebarConfig.show) { + $("body").addClass("right-sidebar-active"); + if (this.options.rightSidebarConfig.visibleDefault) { + $("body").addClass("right-sidebar-visible"); + } + + new RightSidebarView({ + el: "#right-sidebar-container", + router: this.options.router, + rightSidebarConfig: this.options.rightSidebarConfig, + placeConfig: this.options.placeConfig, + layers: this.options.mapConfig.layers, + storyConfig: this.options.storyConfig, + activityConfig: this.options.activityConfig, + activityView: this.activityView, + appView: this, + layerViews: this.mapView.layerViews, + }).render(); + } + }, + + // BEGIN FLAVOR-SPECIFIC CODE + toggleLayerPanel: function() { + $("#sidebar-panel").toggleClass("sidebar-panel--hidden sidebar-panel--visible"); + if ($("#main-btns-container").hasClass("pos-top-left")) { + $("#main-btns-container").toggleClass("main-btns-container--offset-left"); + } + } + // END FLAVOR-SPECIFIC CODE +}); diff --git a/src/flavors/nyrp/static/js/views/gis-legend-view.js b/src/flavors/nyrp/static/js/views/gis-legend-view.js new file mode 100644 index 0000000000..832b9a79e0 --- /dev/null +++ b/src/flavors/nyrp/static/js/views/gis-legend-view.js @@ -0,0 +1,63 @@ +const GISLegendView = require("../../../../../base/static/js/views/gis-legend-view.js"); + +module.exports = GISLegendView.extend({ + + events: { + "change .map-legend-basemap-radio": "toggleBasemap", + "change .map-legend-checkbox": "toggleVisibility", + "change .map-legend-grouping-checkbox": "toggleHeaderVisibility", + "click .info-icon": "onClickInfoIcon", + "click .sidebar-panel__close-panel": "onCloseLayerPanel" + }, + + onCloseLayerPanel: function() { + this.$el.toggleClass("sidebar-panel--hidden sidebar-panel--visible"); + if ($("#main-btns-container").hasClass("pos-top-left")) { + $("#main-btns-container").toggleClass("main-btns-container--offset-left"); + } + }, + + render: function() { + var self = this, + data = _.extend( + { + basemaps: this.options.config.basemaps, + groupings: this.options.config.groupings, + }, + Shareabouts.stickyFieldValues, + ); + + // BEGIN FLAVOR-SPECIFIC CODE + //this.$el.html(Handlebars.templates["gis-legend-content"](data)); + // END FLAVOR-SPECIFIC CODE + + _.each(this.options.config.groupings, function(group) { + _.each(group.layers, function(layer) { + if (layer.constituentLayers) { + layer.constituentLayers.forEach(function(id) { + $(Shareabouts).trigger("visibility", [id, !!layer.visibleDefault]); + }); + } else { + $(Shareabouts).trigger("visibility", [ + layer.id, + !!layer.visibleDefault, + ]); + } + }); + }); + + var initialBasemap = _.find(this.options.config.basemaps, function( + basemap, + ) { + return !!basemap.visibleDefault; + }); + + $(Shareabouts).trigger("visibility", [ + initialBasemap.id, + !!initialBasemap.visibleDefault, + true, + ]); + + return this; + } +}); diff --git a/src/flavors/nyrp/static/js/views/sidebar-view.js b/src/flavors/nyrp/static/js/views/sidebar-view.js new file mode 100644 index 0000000000..1ed7cf0669 --- /dev/null +++ b/src/flavors/nyrp/static/js/views/sidebar-view.js @@ -0,0 +1,25 @@ +const SidebarView = require("../../../../../base/static/js/views/sidebar-view.js"); +const GISLegendView = require("mapseed-gis-legend-view"); + +module.exports = SidebarView.extend({ + + render: function() { + // BEGIN FLAVOR-SPECIFIC CODE + var data = { + + // we assume there's only one panel in this flavor--the gis layers + sidebarConfig: this.options.sidebarConfig.panels[0], + }; + + this.$el.html(Handlebars.templates["sidebar"](data)); + + new GISLegendView({ + el: "#sidebar-panel", + mapView: this.options.mapView, + config: this.options.sidebarConfig.panels[0], + placeConfig: this.options.placeConfig, + sidebarView: this, + }).render(); + // END FLAVOR-SPECIFIC CODE + }, +});