From a43ff1304222e4b059401d6f063529beb50ca9e8 Mon Sep 17 00:00:00 2001 From: Jim Bacon Date: Wed, 2 Aug 2017 14:25:37 +0100 Subject: [PATCH] Fixes #49 - report map doesn't respect template Contains a relatively small amount of code to implement the enhancement. However, includes many style changes and comments to improve legibility. Most significant alteration is to use of the $colsToInclude array which was previously empty unless specifically used to constrain columns sent to JavaScript. Now it is always employed to list output columns. This is because the code also includes a change to prevent output of columns that are marked as not visible (unless required for mapping, e.g. geom) --- report_helper.php | 254 +++++++++++++++++++++++++++++++--------------- 1 file changed, 170 insertions(+), 84 deletions(-) diff --git a/report_helper.php b/report_helper.php index bffe19589..73777ddaf 100644 --- a/report_helper.php +++ b/report_helper.php @@ -1765,65 +1765,91 @@ public static function report_map($options) { 'dataSourceLoRes'=>'', ), $options); $options = self::get_report_grid_options($options); - // keep track of the columns in the report output which we need to draw the layer + + // Keep track of the columns in the report output which we need to draw the layer and popups. $colsToInclude=array(); + if (empty($options['geoserverLayer'])) { - if ($options['ajax']) - // just load the report structure, as Ajax will load content later + // We are doing vector mapping from an Indicia report. + + if ($options['ajax']) { + // Just load the report structure, as Ajax will load content later. $options['extraParams']['limit']=0; + } + self::request_report($response, $options, $currentParamValues, false, ''); - if (isset($response['error'])) return $response['error']; + if (isset($response['error'])) { + // Return immediately on error. + return $response['error']; + } $r = self::params_form_if_required($response, $options, $currentParamValues); - // return the params form, if that is all that is being requested, or the parameters are not complete. - if ($options['paramsOnly'] || !isset($response['records'])) + if ($options['paramsOnly'] || !isset($response['records'])) { + // Return the params form, if that is all that is being requested, or the parameters are not complete. return $r; + } $records = $response['records']; - // find the geom column + + // Find the geom column. foreach($response['columns'] as $col=>$cfg) { if (isset($cfg['mappable']) && $cfg['mappable']=='true') { $wktCol = $col; break; } } - if (!isset($wktCol)) + if (!isset($wktCol)) { $r .= "

".lang::get("The report's configuration does not output any mappable data")."

"; - } else { - // using geoserver, so we just need to know the param values. + } + else { + // Always include geom in output even if marked as not visible in report. + $colsToInclude[$wktCol]=''; + } + + if (isset($options['rowId'])) { + // Always include Id in output even if marked as not visible in report. + $colsToInclude[$options['rowId']] = ''; + } + + } + else { + // We are doing WMS mapping using geoserver, so we just need to know the param values. $currentParamValues = self::get_report_grid_current_param_values($options); $response = self::get_report_data($options, self::array_to_query_string($currentParamValues, true).'&wantRecords=0&wantParameters=1'); $r = self::get_report_grid_parameters_form($response, $options, $currentParamValues); } + if (isset($response['records']) || !isset($response['parameterRequest']) || - count(array_intersect_key($currentParamValues, $response['parameterRequest']))==count($response['parameterRequest'])) { + count(array_intersect_key($currentParamValues, $response['parameterRequest'])) == count($response['parameterRequest'])) { + // We are ready to draw the map. + if (empty($options['geoserverLayer'])) { - // we are doing vector reporting via indicia services - // first we need to build a style object which respects columns in the report output that define style settings for each vector. - // default features are blue and red if selected + // We are doing vector mapping from an Indicia report. + + // Build a default style object which is blue or red if selected. $defsettings = array( - 'fillColor'=> '#0000ff', - 'strokeColor'=> '#0000ff', - 'strokeWidth'=>empty($options['featureDoubleOutlineColour']) ? "\${getstrokewidth}" : 1, - 'fillOpacity'=>"\${getfillopacity}", - 'strokeOpacity'=>0.8, - 'pointRadius'=>"\${getpointradius}", - 'graphicZIndex'=>"\${getgraphiczindex}"); + 'fillColor' => '#0000ff', + 'strokeColor' => '#0000ff', + 'strokeWidth' => empty($options['featureDoubleOutlineColour']) ? "\${getstrokewidth}" : 1, + 'fillOpacity' => "\${getfillopacity}", + 'strokeOpacity' => 0.8, + 'pointRadius' => "\${getpointradius}", + 'graphicZIndex' => "\${getgraphiczindex}"); $selsettings = array_merge($defsettings, array( - 'fillColor'=> '#ff0000', - 'strokeColor'=> '#ff0000', - 'strokeOpacity'=>0.9) + 'fillColor' => '#ff0000', + 'strokeColor' => '#ff0000', + 'strokeOpacity' => 0.9) ); - $defStyleFns=array(); - $selStyleFns=array(); - // default fill opacity, more opaque if selected, and gets more transparent as you zoom in. + $defStyleFns = array(); + $selStyleFns = array(); + // Default fill opacity, more opaque if selected, and gets more transparent as you zoom in. $defStyleFns['fillOpacity'] = "getfillopacity: function(feature) { - return Math.max(0, 0.4-feature.layer.map.zoom/100); + return Math.max(0, 0.4 - feature.layer.map.zoom/100); }"; - // when selected, a little bit more opaque + // When selected, a little bit more opaque. $selStyleFns['fillOpacity'] = "getfillopacity: function(feature) { - return Math.max(0, 0.7-feature.layer.map.zoom/100); + return Math.max(0, 0.7 - feature.layer.map.zoom/100); }"; - // default fill opacity, more opaque if selected, and gets more transparent as you zoom in. + // Default radius based on precision of record but limited to maintain visibility when zoomed out. // Note that the number of map units only approximates a metre in web-mercator, accurate // near the equator but not near the poles. We use a very crude adjustment if necessary // which works well around the UK's latitude. @@ -1834,68 +1860,97 @@ public static function report_map($options) { } return Math.max(5, units / (feature.layer.map.getResolution())); }"; - // default z index, smaller objects on top + // Default z index, smaller objects on top. $defStyleFns['graphicZIndex'] = "getgraphiczindex: function(feature) { - return Math.round(feature.geometry.getBounds().left - feature.geometry.getBounds().right)+100000; + return Math.round(feature.geometry.getBounds().left - feature.geometry.getBounds().right) + 100000; }"; - // when selected, move objects upwards + // When selected, move objects upwards. $selStyleFns['graphicZIndex'] = "getgraphiczindex: function(feature) { - return Math.round(feature.geometry.getBounds().left - feature.geometry.getBounds().right)+200000; + return Math.round(feature.geometry.getBounds().left - feature.geometry.getBounds().right) + 200000; }"; - foreach($response['columns'] as $col=>$def) { + + // Override the default style object using columns in the report output that define style settings. + foreach($response['columns'] as $col => $def) { if (!empty($def['feature_style'])) { - if ($def['feature_style']==='fillOpacity') { - // replace the fill opacity functions to use a column value, with the same +0.3 change - // when selected + // Found a column that defines a style setting. + + if ($def['feature_style'] === 'fillOpacity') { + // Replace the fill opacity functions to use a column value. $defStyleFns['fillOpacity'] = "getfillopacity: function(feature) { - return Math.max(0, feature.attributes.$col-feature.layer.map.zoom/100); + return Math.max(0, feature.attributes.$col - feature.layer.map.zoom/100); }"; + // When selected, a little bit more opaque. $selStyleFns['fillOpacity'] = "getfillopacity: function(feature) { - return Math.max(0, feature.attributes.$col-feature.layer.map.zoom/100+0.3); + return Math.max(0, feature.attributes.$col - feature.layer.map.zoom/100 + 0.3); }"; - } elseif ($def['feature_style']==='graphicZIndex') { - // replace the default z index with the column value, using an fn to add 1000 when selected + } + elseif ($def['feature_style'] === 'graphicZIndex') { + // Replace the default z index with the column value. + // ${} syntax is explained at http://docs.openlayers.org/library/feature_styling.html. $defsettings['graphicZIndex'] = '${'.$col.'}'; + // When selected, move objects upwards. $selStyleFns['graphicZIndex'] = "getgraphiczindex: function(feature) { - return feature.attributes.$col+1000; + return feature.attributes.$col + 1000; }"; $selsettings['graphicZIndex'] = '${getgraphiczindex}'; - } else { - // found a column that outputs data to input into a feature style parameter. ${} syntax is explained at http://docs.openlayers.org/library/feature_styling.html. + } + else { + // Found a column that outputs data to input into a feature style parameter. + // ${} syntax is explained at http://docs.openlayers.org/library/feature_styling.html. $defsettings[$def['feature_style']] = '${'.$col.'}'; - if ($def['feature_style']!=='strokeColor') + if ($def['feature_style'] !== 'strokeColor') { + // Override the style for selected items too except red stroke colour. $selsettings[$def['feature_style']] = '${'.$col.'}'; + } } + + // We need to include in output any columns involved in the feature style. + $colsToInclude[$col] = ''; } } - if ($options['displaySymbol']!=='vector') - $defsettings['graphicName']=$options['displaySymbol']; - // The following function uses the strokeWidth to pad out the squares which go too small when zooming the map out. Points - // always display the same size so are no problem. Also, no need if using a double outline. + + if ($options['displaySymbol'] !== 'vector') { + // Use a symbol marker on the map rather than vector. + $defsettings['graphicName'] = $options['displaySymbol']; + } + + // The following function uses the strokeWidth to pad out the squares which go too small when zooming the map + // out. Points always display the same size so are no problem. Also, no need if using a double outline. if (empty($options['featureDoubleOutlineColour'])) { $strokeWidthFn = "getstrokewidth: function(feature) { - var width=feature.geometry.getBounds().right - feature.geometry.getBounds().left, - strokeWidth=(width===0) ? 1 : %d - (width / feature.layer.map.getResolution()); - return (strokeWidth<%d) ? %d : strokeWidth; + var width = feature.geometry.getBounds().right - feature.geometry.getBounds().left, + strokeWidth = (width === 0) ? 1 : %d - (width / feature.layer.map.getResolution()); + return (strokeWidth < %d) ? %d : strokeWidth; }"; $defStyleFns['getStrokeWidth'] = sprintf($strokeWidthFn, 9, 2, 2); $selStyleFns['getStrokeWidth'] = sprintf($strokeWidthFn, 10, 3, 3); } + + // The style objects can also be overriden by column values as specified in the valueOutput option. if (isset($options['valueOutput'])) { foreach($options['valueOutput'] as $type => $outputdef) { $value = $outputdef['valueField']; - // we need this value in the output + // We need this value in the output. $colsToInclude[$value]=''; + if (preg_match('/{(?P.+)}/', $outputdef['minValue'], $matches)) { + // Min value is obtained from column. $minvalue = 'feature.data.'.$matches['name']; $colsToInclude[$matches['name']]=''; - } else + } + else { $minvalue = $outputdef['minValue']; + } + if (preg_match('/{(?P.+)}/', $outputdef['maxValue'], $matches)) { + // Max value is obtained frm column. $maxvalue = 'feature.data.'.$matches['name']; $colsToInclude[$matches['name']]=''; - } else + } + else { $maxvalue = $outputdef['maxValue']; + } + $from = $outputdef['from']; $to = $outputdef['to']; if (substr($type, -5)==='Color') @@ -1919,34 +1974,33 @@ public static function report_map($options) { '}'; $defsettings[$type]="\${get$type}"; } - // As well as the columns required for valueOutput, we also need to include any others - // involved in the feature style - foreach ($response['columns'] as $col=>$cfg) { - if (!empty($cfg['feature_style'])) - $colsToInclude[$col] = ''; - } } + + // Convert these styles into a JSON definition ready to feed into JS. $selStyleFns = implode(",\n", array_values(array_merge($defStyleFns, $selStyleFns))); $defStyleFns = implode(",\n", array_values($defStyleFns)); - // convert these styles into a JSON definition ready to feed into JS. + $defStyleFns = ", {context: {\n $defStyleFns\n }}"; + $selStyleFns = ", {context: {\n $selStyleFns\n }}"; $defsettings = json_encode($defsettings); $selsettings = json_encode($selsettings); + $addFeaturesJs = ""; // No need to pass the default type of vector display, so use empty obj to keep JavaScript size down - $opts = $options['displaySymbol']==='vector' ? '{}' : json_encode(array('type'=>$options['displaySymbol'])); - if ($options['clickableLayersOutputMode']!=='popup' && $options['clickableLayersOutputMode']!=='div' - && $options['clickableLayersOutputMode']!=='customFunction' && isset($wktCol)) { - // If we don't need record data for every row for feature clicks, then only include necessary columns to minimise JS - $colsToInclude['occurrence_id']=''; - $colsToInclude[$wktCol]=''; - foreach ($response['columns'] as $name=>$def) { - if (isset($def['feature_style'])) + $opts = $options['displaySymbol'] === 'vector' ? '{}' : json_encode(array('type' => $options['displaySymbol'])); + + if ($options['clickableLayersOutputMode'] == 'popup' || $options['clickableLayersOutputMode'] == 'div' + || $options['clickableLayersOutputMode'] == 'customFunction') { + // Add in all other visible columns from report if needed for feature clicks. They can be omitted otherwise + // to minimise JS. + foreach ($response['columns'] as $name => $cfg) { + if (!isset($cfg['visible']) || ($cfg['visible'] !== 'false' && $cfg['visible'] !== false)) { $colsToInclude[$name] = ''; + } } } - $defStyleFns = ", {context: {\n $defStyleFns\n }}"; - $selStyleFns = ", {context: {\n $selStyleFns\n }}"; + if ($options['ajax']) { + // Output scripts to get map loading by Ajax. self::$javascript .= "mapInitialisationHooks.push(function(div) {\n". " var wantToMap = typeof indiciaData.filter==='undefined' || typeof indiciaData.filter.def.indexed_location_id==='undefined' || indiciaData.filter.def.indexed_location_id==='';\n" . " if (wantToMap && typeof indiciaData.reports!==\"undefined\") {\n" . @@ -1971,31 +2025,62 @@ public static function report_map($options) { " }});\n"; } self::$javascript .= "});\n"; - } else { + } + else { + // Not Ajax so output data to be loaded. $geoms = array(); - foreach ($records as $record) { + $imagePath = self::get_uploaded_image_folder(); + foreach ($records as $record) { + // Loop through all records. + if (isset($wktCol) && !empty($record[$wktCol])) { - $record[$wktCol]=preg_replace('/\.(\d+)/', '', $record[$wktCol]); - // rather than output every geom separately, do a list of distinct geoms to minify the JS + // Only ouput records which can be mapped. + + // Truncate fractional parts of WKT + $record[$wktCol] = preg_replace('/\.(\d+)/', '', $record[$wktCol]); + // Rather than output every geom separately, do a list of distinct geoms. if (!$geomIdx = array_search('"'.$record[$wktCol].'"', $geoms)) { $geoms[] = '"'.$record[$wktCol].'"'; - $geomIdx = count($geoms)-1; + $geomIdx = count($geoms) - 1; } $record[$wktCol] = $geomIdx; + + // Process image columns and templated columns as defined in report file. + foreach ($response['columns'] as $col => $cfg) { + if (isset($cfg['img']) && $cfg['img']=='true' && !empty($record[$col]) && !isset($cfg['template'])) { + // Output thumbnails from image columns + $imgs = explode(',', $record[$col]); + $value=''; + foreach($imgs as $img) { + $value .= ""; + } + } + elseif (isset($cfg['template'])) { + // Build value from template in report. + $value = self::mergeParamsIntoTemplate($record, $cfg['template'], true, true, true); + } + else { + // Don't display any null values returned. + $value = isset($record[$col]) ? $record[$col] : ''; + } + $record[$col] = $value; + } + if (!empty($colsToInclude)) { - // if limiting the columns, ensure that the geom and row ID are included. - $colsToInclude[$wktCol]=''; - if (!empty($options['rowId'])) - $colsToInclude[$options['rowId']]=''; + // Remove all columns from record which are not needed. $record = array_intersect_key($record, $colsToInclude); } + $addFeaturesJs.= "div.addPt(features, ".json_encode($record).", '$wktCol', $opts" . (empty($options['rowId']) ? '' : ", '" . $record[$options['rowId']] . "'") . ");\n"; } } self::$javascript .= 'indiciaData.geoms=['.implode(',',$geoms)."];\n"; } + + self::addFeaturesLoadingJs($addFeaturesJs, $defsettings, $selsettings, $defStyleFns, $selStyleFns, $options['zoomMapToOutput'] && !$options['ajax'], $options['featureDoubleOutlineColour']); - } else { + } + else { // doing WMS reporting via GeoServer $replacements = array(); foreach(array_keys($currentParamValues) as $key) @@ -2019,6 +2104,7 @@ public static function report_map($options) { $filter, $style}, {singleTile: true, isBaseLayer: false, sphericalMercator: true});\n"; } + $setLocationJs = ''; //When the user uses a page like dynamic report explorer with a map, then there might be more than //one parameter that is a location based parameter. For instance, Site and Region might be seperate parameters,