diff --git a/R/app.R b/R/app.R index 597e673..0d09dc5 100644 --- a/R/app.R +++ b/R/app.R @@ -1,12 +1,19 @@ #' @import shiny dplyr geocatApp <- function(...) { timeoutTime = 15 + circleRadius = 7 + customCol = "#ECAC7C" + gbifCol = "#509E2F" + csvCol = "#0078b4" + gbifOffCol = "#a5cd96" + csvOffCol = "#7fbbd9" + #### ui #### ui <- fluidPage( theme = shinythemes::shinytheme("darkly"), shinyjs::useShinyjs(), - tags$html(lang = "en", `data-timeout-mins` = timeoutTime), + tags$html(lang = "en", `data-timeout-mins` = timeoutTime, `data-circle-radius` = circleRadius), tags$head( tags$title("ShinyGeoCAT - Geospatial Conservation Assessment Tools"), # WCAG modification tags$link(rel = "stylesheet", href = "style.css"), @@ -393,11 +400,11 @@ geocatApp <- function(...) { targetGroup = 'mappoints', circleMarkerOptions=drawCircleMarkerOptions( color="#FFFFFF", - radius=7, + radius = circleRadius, stroke=T, weight=2.5, fill=T, - fillColor="#ECAC7C", + fillColor=customCol, opacity=1, fillOpacity=0.5, repeatMode = TRUE, @@ -500,11 +507,11 @@ geocatApp <- function(...) { lng = long, lat = lat, color="#FFFFFF", - radius=7, + radius = circleRadius, stroke=T, weight=2.5, fill=T, - fillColor="#ECAC7C", + fillColor=customCol, opacity=1, fillOpacity=0.5 ) @@ -675,12 +682,12 @@ geocatApp <- function(...) { points <- filter(values$analysis_data, geocat_source != "User point") used_pal <- colorFactor( - palette=c("#509E2F", "#0078b4"), + palette=c(gbifCol, csvCol), domain=c("GBIF", "User CSV") ) unused_pal <- colorFactor( - palette=c("#a5cd96", "#7fbbd9"), + palette=c(gbifOffCol, csvOffCol), domain=c("GBIF", "User CSV") ) @@ -701,7 +708,7 @@ geocatApp <- function(...) { popup = ~pcontent, layerId = ~geocat_id, group="mappoints", - radius = 7, + radius = circleRadius, color="#FFFFFF", stroke = T, weight = 2.5, @@ -709,13 +716,13 @@ geocatApp <- function(...) { fillColor = ~used_pal(geocat_source), fillOpacity = 0.5, #options = markerOptions(draggable = FALSE), - options = pathOptions(pane = "mappoints"), + options = pathOptions(pane = "mappoints", className = ~factor(geocat_source, levels=c("GBIF", "User CSV"), labels=c("shape-square", "shape-hexagon"))), data=used_points ) %>% leaflet::addCircleMarkers( layerId = ~geocat_id, group="unmappoints", - radius = 7, + radius = circleRadius, color="#BBBBBB", stroke = T, weight = 2, @@ -723,7 +730,7 @@ geocatApp <- function(...) { fillColor = ~unused_pal(geocat_source), fillOpacity = 0.2, #options = markerOptions(draggable = FALSE), - options = pathOptions(pane = "unmappoints"), + options = pathOptions(pane = "unmappoints", className = ~factor(geocat_source, levels=c("GBIF", "User CSV"), labels=c("shape-square", "shape-hexagon"))), data=unused_points ) }) diff --git a/www/js/map.js b/www/js/map.js index 03d9758..9b98ec2 100644 --- a/www/js/map.js +++ b/www/js/map.js @@ -1,11 +1,13 @@ - -function fixA11yIssues () { +function fixSearchInput() { // Search bar should be of type search rather than have that role $('.search-input') - .attr('type', 'search') - .removeAttr('role'); + .attr('type', 'search') + .removeAttr('role'); +} - // The rest of this function makes sure tabbing around works properly. + +function fixKeyboard() { + // Make sure tabbing around works properly. // Specifically, the focus doesn't get lost when opening or closing // the layers popup const $searchButton = $('.leaflet-control-search') @@ -39,7 +41,7 @@ function fixA11yIssues () { evt.preventDefault(); }); - + // Add floating dialog for keyboard users const setupFloatingDialog = function($control, $dialog) { let clicking = false; @@ -98,6 +100,100 @@ function fixA11yIssues () { } +function fixShapes() { + const $html = $(document.documentElement); + const circleRadius = parseFloat($html.attr('data-circle-radius')); + + const area = Math.PI * Math.pow(circleRadius, 2); + + + const createPathTransformer = function(edgeLength, coords) { + const getPathMid = path => `M${path.split(/[A-Z]/i)[1]} m ${circleRadius}, 0`; + + const end = coords.map(function([x, y], i) { + if (i === 0) { + return `m ${x * edgeLength}, ${y * edgeLength}`; + } + const [px, py] = coords[i-1]; + const dx = (x - px) * edgeLength; + const dy = (y - py) * edgeLength; + const z = i === (coords.length - 1) ? 'Z' : ''; + return `l ${dx}, ${dy} ${z}`; + }, ''); + + return function(path) { + const start = getPathMid(path); + return `${start} ${end}`; + } + } + + + const squarifyPath = (function() { + const edgeLength = Math.sqrt(area); + + const coords = [ + [-1/2, -1/2], + [1/2, -1/2], + [1/2, 1/2], + [-1/2, 1/2] + ]; + + return createPathTransformer(edgeLength, coords); + })(); + + + const hexifyPath = (function() { + const edgeLength = Math.sqrt((2*area) / (3*Math.sqrt(3))); + const rt3o2 = Math.sqrt(3) / 2; + + const coords = [ + [-1/2, -rt3o2], + [1/2, -rt3o2], + [1, 0], + [1/2, rt3o2], + [-1/2, rt3o2], + [-1, 0] + ]; + + return createPathTransformer(edgeLength, coords); + })(); + + const transformMarkerPath = function(marker, shape) { + const path = marker.getAttribute('d'); + if (path === "M0 0") { return; } // offscreen + if (path === marker.savedPath) { return; } // already altered shape + const pathTransformer = { square: squarifyPath, hexagon: hexifyPath }[shape]; + marker.savedPath = pathTransformer(path); + marker.setAttribute('d', marker.savedPath); + } + + const childListMutateCallback = function (mutationList) { + mutationList.forEach(function(d) { + const mutatedElements = d.type === 'childList' ? d.addedNodes : [d.target]; + mutatedElements.forEach(function(el) { + const $el = $(el); + if ($el.hasClass('shape-square')) { transformMarkerPath(el, 'square'); } + else if ($el.hasClass('shape-hexagon')) { transformMarkerPath(el, 'hexagon'); } + }); + }); + } + + const $panes = $('#mymap :is(.leaflet-mappoints-pane, .leaflet-unmappoints-pane)'); + + const elementMutatedObserver = new MutationObserver(childListMutateCallback); + const config = { childList: true, subtree: true, attributes: true, attributeList: ['d'] }; + $panes.each(function() { elementMutatedObserver.observe(this, config); }); + +} + + +function fixA11yIssues () { + fixSearchInput(); + fixKeyboard(); + fixShapes(); +} + + function elementAppendedMutation(mutationList, observer) { const mutation = mutationList[0]; const $target = $(mutation.target);