From adaee71b99420e19360ba38a4513e60e809dd0e3 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Thu, 17 Mar 2016 14:18:20 +1100 Subject: [PATCH 01/21] Fix parsing Status code in readParadoppBinary. --- Parser/readParadoppBinary.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Parser/readParadoppBinary.m b/Parser/readParadoppBinary.m index b982026ae..397e95508 100644 --- a/Parser/readParadoppBinary.m +++ b/Parser/readParadoppBinary.m @@ -448,7 +448,7 @@ sect.PressureMSB = data(idx+24); % uint8 % 8 bits status code http://cs.nortek.no/scripts/customer.fcgi?_sf=0&custSessionKey=&customerLang=en&noCookies=true&action=viewKbEntry&id=7 -sect.Status = dec2bin(bytecast(data(idx+25), 'L', 'int8', cpuEndianness), 8)'; +sect.Status = dec2bin(bytecast(data(idx+25), 'L', 'uint8', cpuEndianness), 8)'; sect.PressureLSW = data(idx+26:idx+27); % uint16 % !!! temperature and velocity can be negative @@ -630,7 +630,7 @@ sect.Error = bytecast(data(idx+22), 'L', 'int8', cpuEndianness); % 8 bits status code http://cs.nortek.no/scripts/customer.fcgi?_sf=0&custSessionKey=&customerLang=en&noCookies=true&action=viewKbEntry&id=7 -sect.Status = dec2bin(bytecast(data(idx+25), 'L', 'int8', cpuEndianness), 8)'; +sect.Status = dec2bin(bytecast(data(idx+25), 'L', 'uint8', cpuEndianness), 8)'; sect.Analn = data(idx+24:idx+25); % uint16 sect.Checksum = data(idx+26:idx+27); % uint16 @@ -669,7 +669,7 @@ sect.PressureMSB = bytecast(data(idx+24), 'L', 'uint8', cpuEndianness); % uint8 % 8 bits status code http://cs.nortek.no/scripts/customer.fcgi?_sf=0&custSessionKey=&customerLang=en&noCookies=true&action=viewKbEntry&id=7 -sect.Status = dec2bin(bytecast(data(idx+25), 'L', 'int8', cpuEndianness), 8)'; +sect.Status = dec2bin(bytecast(data(idx+25), 'L', 'uint8', cpuEndianness), 8)'; sect.PressureLSW = data(idx+26:idx+27); % uint16 sect.Temperature = data(idx+28:idx+29); % int16 @@ -832,7 +832,7 @@ sect.Roll = block(3); sect.PressureMSB = bytecast(data(idx+24), 'L', 'uint8', cpuEndianness); % uint8 % 8 bits status code http://cs.nortek.no/scripts/customer.fcgi?_sf=0&custSessionKey=&customerLang=en&noCookies=true&action=viewKbEntry&id=7 -sect.Status = dec2bin(bytecast(data(idx+25), 'L', 'int8', cpuEndianness), 8)'; +sect.Status = dec2bin(bytecast(data(idx+25), 'L', 'uint8', cpuEndianness), 8)'; sect.PressureLSW = bytecast(data(idx+ 26:idx+27), 'L', 'uint16', cpuEndianness); % uint16 sect.Temperature = bytecast(data(idx+ 28:idx+29), 'L', 'int16', cpuEndianness); % int16 % bytes 30-117 are spare From 585b9a4edff2272af8be85afdac877cfbae35535 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Thu, 17 Mar 2016 14:23:05 +1100 Subject: [PATCH 02/21] Avoid warning generated if user tries to delete a file from an empty deployment file list. --- GUI/dataFileStatusDialog.m | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/GUI/dataFileStatusDialog.m b/GUI/dataFileStatusDialog.m index 635c510ba..bc80295d6 100644 --- a/GUI/dataFileStatusDialog.m +++ b/GUI/dataFileStatusDialog.m @@ -245,14 +245,16 @@ function fileRemoveCallback(source,ev) dep = get(depList, 'Value'); file = get(fileList, 'Value'); - files{dep}(file) = []; - - set(fileList, 'String', files{dep}, 'Value', 1); - - % update deplist view (the deployment's status may have changed) - set(depList, ... - 'Value', dep, ... - 'String', annotateDepDescriptions(deployments, deploymentDescs)); + if ~isempty(files{dep}) + files{dep}(file) = []; + + set(fileList, 'String', files{dep}, 'Value', 1); + + % update deplist view (the deployment's status may have changed) + set(depList, ... + 'Value', dep, ... + 'String', annotateDepDescriptions(deployments, deploymentDescs)); + end end function fileAddCallback(source,ev) From 5e070bd5bb78c8ffbd7c1ae62546a72358c4ec53 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Thu, 17 Mar 2016 18:17:31 +1100 Subject: [PATCH 03/21] Fix serial number parsing in aquatec files. --- Parser/aquatecParse.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Parser/aquatecParse.m b/Parser/aquatecParse.m index 058386217..339286ab4 100644 --- a/Parser/aquatecParse.m +++ b/Parser/aquatecParse.m @@ -125,12 +125,13 @@ model = strtrim(strrep(model, 'Temperature', '')); firmware = getValues({'VERSION'}, keys, meta); serial = getValues({'LOGGER'}, keys, meta); +serial = textscan(serial{1}, '%s', 1, 'Delimiter', ','); sample_data.toolbox_input_file = filename; sample_data.meta.instrument_make = 'Aquatec'; sample_data.meta.instrument_model = ['Aqualogger ' model{1}]; sample_data.meta.instrument_firmware = firmware{1}; -sample_data.meta.instrument_serial_no = serial{1}; +sample_data.meta.instrument_serial_no = serial{1}{1}; sample_data.meta.featureType = mode; % % get regime data (mode, sample rate, etc) From a35092a46e87d611aba7941c3a84ed30893b4e1d Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Thu, 17 Mar 2016 18:24:35 +1100 Subject: [PATCH 04/21] Better description of dataset in combo list of main window. --- Util/genSampleDataDesc.m | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Util/genSampleDataDesc.m b/Util/genSampleDataDesc.m index 07f350354..3200d9a9c 100644 --- a/Util/genSampleDataDesc.m +++ b/Util/genSampleDataDesc.m @@ -49,7 +49,7 @@ timeFmt = readProperty('toolbox.timeFormat'); -timeRange = [datestr(sam.time_coverage_start, timeFmt) ' - ' ... +timeRange = ['from ' datestr(sam.time_coverage_start, timeFmt) ' to ' ... datestr(sam.time_coverage_end, timeFmt)]; [fPath fName fSuffix] = fileparts(sam.toolbox_input_file); @@ -57,8 +57,9 @@ fName = [fName fSuffix]; desc = [ sam.meta.site_id ... - ' (' sam.meta.instrument_make ... + ' - ' sam.meta.instrument_make ... ' ' sam.meta.instrument_model ... - ' ' sam.meta.instrument_serial_no ... - ' - ' fName .... - ') ' timeRange]; + ' SN=' sam.meta.instrument_serial_no ... + ' @' num2str(sam.meta.depth) 'm' ... + ' ' timeRange ... + ' (' fName ')']; From be34f363edbea5dfe7e45d412b8d7a88e7fd1db0 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Thu, 17 Mar 2016 18:26:47 +1100 Subject: [PATCH 05/21] Rearrange list of dataset sorted by nominal depth. Description of dataset improved with nominal depth info when importing files. --- GUI/dataFileStatusDialog.m | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/GUI/dataFileStatusDialog.m b/GUI/dataFileStatusDialog.m index bc80295d6..ce36ab450 100644 --- a/GUI/dataFileStatusDialog.m +++ b/GUI/dataFileStatusDialog.m @@ -66,13 +66,14 @@ deploymentDescs = genDepDescriptions(deployments, files); - % put deployments in alphabetical order + % sort deployments by depth % % [B, iX] = sort(A); % => % A(iX) == B % - [deploymentDescs, iSort] = sort(deploymentDescs); + [~, iSort] = sort([deployments.InstrumentDepth]); + deploymentDescs = deploymentDescs(iSort); deployments = deployments(iSort); files = files(iSort); @@ -311,11 +312,18 @@ function fileAddCallback(source,ev) dep = deployments(k); - % at the very least, the instrument and file name + % at the very least, the instrument, nominal depth and file name descs{k} = dep.InstrumentID; + if ~isempty(dep.InstrumentDepth) + descs{k} = [descs{k} ' @' num2str(dep.InstrumentDepth) 'm']; + end + if ~isempty(dep.DepthTxt) + descs{k} = [descs{k} ' ' dep.DepthTxt]; + end if ~isempty(dep.FileName) descs{k} = [descs{k} ' (' dep.FileName ')']; end + % get some site information if it exists site = executeDDBQuery('Sites', 'Site', dep.Site); From 54f4df699619fc956b40e6e0645b1cebf31dcb65 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Tue, 22 Mar 2016 12:11:24 +1100 Subject: [PATCH 06/21] RDI ADCPs now have there timestamps correct, lying in the middle of the burst instead of the beginning. --- Parser/workhorseParse.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Parser/workhorseParse.m b/Parser/workhorseParse.m index 64f1ac154..0edcc8ebd 100644 --- a/Parser/workhorseParse.m +++ b/Parser/workhorseParse.m @@ -138,6 +138,9 @@ variable.y2kMinute,... variable.y2kSecond + variable.y2kHundredth/100.0]); + % shift the timestamp to the middle of the burst + time = time + (fixed.pingsPerEnsemble .* (fixed.tppMinutes*60 + fixed.tppSeconds + fixed.tppHundredths/100) / (3600 * 24))/2; + % % auxillary data % From d04aa3ab58812e0619c74d0eb3d124d7c7fb5189 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Tue, 22 Mar 2016 12:17:15 +1100 Subject: [PATCH 07/21] signature interleaved burst data is the same format as burst/average data and is now supported in readAD2CPBinary. --- Parser/readAD2CPBinary.m | 52 +++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/Parser/readAD2CPBinary.m b/Parser/readAD2CPBinary.m index 79f539948..98c2e902a 100644 --- a/Parser/readAD2CPBinary.m +++ b/Parser/readAD2CPBinary.m @@ -4,7 +4,7 @@ % % This function is able to parse raw binary data from any Nortek instrument % which is defined in the Data formats chapter of the Nortek -% Integrator Guide AD2CP, 2015. +% Integrator Guide AD2CP, 2016. % % Nortek AD2CP binary files consist of 2 'sections': header and data record, the format of % which are specified in the Integrator Guide. This function reads @@ -147,14 +147,13 @@ case '17' [sect, len, off] = readBottomTrack (data, idx, cpuEndianness); % 0x17 case '18' - % [sect, len, off] = readInterleavedBurst (data, idx, cpuEndianness); % 0x18 - disp('Interleaved Burst Data Record not supported'); - disp(['ID : hex ' id ' at offset ' num2str(idx+2)]); + [sect, len, off] = readBurstAverage (data, idx, cpuEndianness); % 0x18 case 'A0' [sect, len, off] = readString (data, idx, cpuEndianness); % 0xA0 otherwise disp('Unknown section type'); disp(['ID : hex ' id ' at offset ' num2str(idx+2)]); + off = len; end if isempty(sect), return; end @@ -220,7 +219,7 @@ function [sect, len, off] = readBurstAverage(data, idx, cpuEndianness) %READBURST -% Id=0x15, Burst/Average Data Record +% Burst/Average Data Record sect = struct; [sect.Header, len, off] = readHeader(data, idx, cpuEndianness); @@ -249,7 +248,7 @@ function sect = readBurstAverageVersion1(data, idx, cpuEndianness) %READBURSTAVERAGEVERSION1 -% Id=0x15 or 0x16, Burst/Average Data Record +% Id=0x15 or 0x16 or 0x18, Burst/Average Data Record % Version=1 sect = struct; @@ -306,7 +305,7 @@ function sect = readBurstAverageVersion2(data, idx, cpuEndianness) %READBURSTAVERAGEVERSION2 -% Id=0x15 or 0x16, Burst/Average Data Record +% Id=0x15 or 0x16 or 0x18, Burst/Average Data Record % Version=2 sect = struct; @@ -375,7 +374,7 @@ function sect = readBurstAverageVersion3(data, idx, cpuEndianness) %READBURSTAVERAGEVERSION3 -% Id=0x15 or 0x16, Burst/Average Data Record +% Id=0x15 or 0x16 or 0x18, Burst/Average Data Record % Version=3 sect = struct; @@ -405,8 +404,11 @@ iEndCell = 10; iStartBeam = 13; iEndBeam = 16; +iStartCoord = 11; +iEndCoord = 12; sect.nCells = bin2dec(sect.Beams_CoordSys_Cells(end-iEndCell+1:end-iStartCell+1)); sect.nBeams = bin2dec(sect.Beams_CoordSys_Cells(end-iEndBeam+1:end-iStartBeam+1)); +sect.coordSys = sect.Beams_CoordSys_Cells(end-iEndCoord+1:end-iStartCoord+1); sect.CellSize = bytecast(data(idx+32:idx+33), 'L', 'uint16', cpuEndianness); % 1 mm sect.Blanking = bytecast(data(idx+34:idx+35), 'L', 'uint16', cpuEndianness); % 1 mm @@ -430,7 +432,7 @@ sect.Status = dec2bin(bytecast(data(idx+68:idx+71), 'L', 'uint32', cpuEndianness), 32); sect.EnsembleCounter = bytecast(data(idx+72:idx+75), 'L', 'uint32', cpuEndianness); % counts the number of ensembles in both averaged and burst data -off = 75; +off = sect.OffsetOfData - 1; if isVelocity sect.VelocityData = reshape(bytecast(data(idx+off+1:idx+off+sect.nBeams*sect.nCells*2), 'L', 'int16', cpuEndianness), sect.nCells, sect.nBeams)'; % 10^(velocity scaling) m/s off = off+sect.nBeams*sect.nCells*2; @@ -447,25 +449,25 @@ end if isAltimeter - off = off + 1; - sect.AltimeterDistance = bytecast(data(idx+off:idx+off+3), 'L', 'single', cpuEndianness); % m - off = off + 3; - sect.AltimeterQuality = bytecast(data(idx+off:idx+off+1), 'L', 'uint16', cpuEndianness); - off = off + 1; - sect.AltimeterStatus = dec2bin(bytecast(data(idx+off:idx+off+1), 'L', 'uint16', cpuEndianness), 16); - off = off + 1; + sect.AltimeterDistance = bytecast(data(idx+off+1:idx+off+4), 'L', 'single', cpuEndianness); % m + off = off + 4; + sect.AltimeterQuality = bytecast(data(idx+off+1:idx+off+2), 'L', 'uint16', cpuEndianness); + off = off + 2; + sect.AltimeterStatus = dec2bin(bytecast(data(idx+off+1:idx+off+2), 'L', 'uint16', cpuEndianness), 16); + off = off + 2; end if isAST - off = off + 1; - sect.ASTDistance = bytecast(data(idx+off:idx+off+3), 'L', 'single', cpuEndianness); % m - off = off + 3; - sect.ASTQuality = bytecast(data(idx+off:idx+off+1), 'L', 'uint16', cpuEndianness); - off = off + 1; - sect.ASTOffset100uSec = bytecast(data(idx+off:idx+off+1), 'L', 'int16', cpuEndianness); % 100 us - off = off + 1; - sect.ASTPressure = bytecast(data(idx+off:idx+off+3), 'L', 'single', cpuEndianness); % dbar - off = off + 3; + sect.ASTDistance = bytecast(data(idx+off+1:idx+off+4), 'L', 'single', cpuEndianness); % m + off = off + 4; + sect.ASTQuality = bytecast(data(idx+off+1:idx+off+2), 'L', 'uint16', cpuEndianness); + off = off + 2; + sect.ASTOffset100uSec = bytecast(data(idx+off+1:idx+off+2), 'L', 'int16', cpuEndianness); % 100 us + off = off + 2; + sect.ASTPressure = bytecast(data(idx+off+1:idx+off+4), 'L', 'single', cpuEndianness); % dbar + off = off + 4; + sect.ASTSpare = bytecast(data(idx+off+1:idx+off+8), 'L', 'int8', cpuEndianness); % spare + off = off + 8; end end From 614e95ab278f79b5f7f199ccc50d19a6f49f3aa5 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Wed, 23 Mar 2016 17:38:41 +1100 Subject: [PATCH 08/21] Added timeDriftPP to correct clock drift from post calibration information. --- Preprocessing/timeDriftPP.m | 336 ++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 Preprocessing/timeDriftPP.m diff --git a/Preprocessing/timeDriftPP.m b/Preprocessing/timeDriftPP.m new file mode 100644 index 000000000..790865641 --- /dev/null +++ b/Preprocessing/timeDriftPP.m @@ -0,0 +1,336 @@ +function sample_data = timeDriftPP(sample_data, qcLevel, auto) +%TIMEDRIFTPP Prompts the user to apply time drift correction to the given data +% sets. A pre-deployment time offset and end deployment time offset are +% required and if included in the DDB (or CSV file), will be shown in the +% dialog box. Otherwise, user is required to enter them. +% +% Offsets should be entered in seconds from UTC time (instrumentTime - UTCtime). +% An offset at the start will result in an offset to the start time. Global +% attributes of time coverage are also adjusted. +% Time drift calculations are applied to both raw and QC datasets. +% All IMOS datasets should be provided in UTC time. Raw data may not +% necessarily have been captured in UTC time, so a correction must be made +% before the data can be considered to be in an IMOS compatible format. +% +% +% Inputs: +% sample_data - cell array of structs, the data sets to which time +% correction should be applied. +% qcLevel - string, 'raw' or 'qc'. Some pp not applied when 'raw'. +% auto - logical, run pre-processing in batch mode. +% +% Outputs: +% sample_data - same as input, with time correction applied. +% + +% +% Author: Rebecca Cowley +% Contributor: Guillaume Galibert +% + +% +% Copyright (c) 2009, eMarine Information Infrastructure (eMII) and Integrated +% Marine Observing System (IMOS). +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright +% notice, this list of conditions and the following disclaimer in the +% documentation and/or other materials provided with the distribution. +% * Neither the name of the eMII/IMOS nor the names of its contributors +% may be used to endorse or promote products derived from this software +% without specific prior written permission. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% + +narginchk(2,3); + +if ~iscell(sample_data), error('sample_data must be a cell array'); end +if isempty(sample_data), return; end +% if this is the second time through (ie for applying autoQC PP +% routines), then return. sample_data already contains the corrections +% and they are therefore carried through. +if auto, return; end + +% auto logical in input to enable running under batch processing +if nargin<3, auto=false; end + +ddb = readProperty('toolbox.ddb'); + +descs = {}; +nSample = length(sample_data); +startOffsets = zeros(nSample, 1); +endOffsets = startOffsets; +sets = ones(nSample, 1); + +% create descriptions, and get timezones/offsets for each data set +for k = 1:nSample + + descs{k} = genSampleDataDesc(sample_data{k}); + + %check to see if the offsets are available already from the ddb + if strcmp('csv',ddb) + if isfield(sample_data{k}.meta.deployment,'StartOffset') + startOffsets(k) = sample_data{k}.meta.deployment.StartOffset; + end + end + + if strcmp('csv',ddb) + if isfield(sample_data{k}.meta.deployment,'EndOffset') + endOffsets(k) = sample_data{k}.meta.deployment.EndOffset; + end + else + if all(isfield(sample_data{k}.meta.deployment,{'TimeDriftInstrument', 'TimeDriftGPS'})) + if ~isempty(sample_data{k}.meta.deployment.TimeDriftInstrument) && ~isempty(sample_data{k}.meta.deployment.TimeDriftGPS) + endOffsets(k) = (sample_data{k}.meta.deployment.TimeDriftInstrument - sample_data{k}.meta.deployment.TimeDriftGPS)*3600*24; + end + end + end +end + +f = figure(... + 'Name', 'Time start and end drift calculations in seconds',... + 'Visible', 'off',... + 'MenuBar' , 'none',... + 'Resize', 'off',... + 'WindowStyle', 'Modal',... + 'NumberTitle', 'off'); + +cancelButton = uicontrol('Style', 'pushbutton', 'String', 'Cancel'); +confirmButton = uicontrol('Style', 'pushbutton', 'String', 'Ok'); + +setCheckboxes = []; +startOffsetFields = []; +endOffsetFields = []; + +for k = 1:nSample + + setCheckboxes(k) = uicontrol(... + 'Style', 'checkbox',... + 'String', descs{k},... + 'Value', 1, ... + 'UserData', k); + + startOffsetFields(k) = uicontrol(... + 'Style', 'edit',... + 'UserData', k, ... + 'String', num2str(startOffsets(k))); + + endOffsetFields(k) = uicontrol(... + 'Style', 'edit',... + 'UserData', k, ... + 'String', num2str(endOffsets(k))); + +end + +% set all widgets to normalized for positioning +set(f, 'Units', 'normalized'); +set(cancelButton, 'Units', 'normalized'); +set(confirmButton, 'Units', 'normalized'); +set(setCheckboxes, 'Units', 'normalized'); +set(startOffsetFields, 'Units', 'normalized'); +set(endOffsetFields, 'Units', 'normalized'); + +set(f, 'Position', [0.2 0.35 0.6 0.0222*nSample]); +set(cancelButton, 'Position', [0.0 0.0 0.5 0.1]); +set(confirmButton, 'Position', [0.5 0.0 0.5 0.1]); + +rowHeight = 0.9 / nSample; +for k = 1:nSample + + rowStart = 1.0 - k * rowHeight; + + set(setCheckboxes (k), 'Position', [0.0 rowStart 0.6 rowHeight]); + set(startOffsetFields (k), 'Position', [0.6 rowStart 0.2 rowHeight]); + set(endOffsetFields (k), 'Position', [0.8 rowStart 0.2 rowHeight]); +end + +% set back to pixels +set(f, 'Units', 'normalized'); +set(cancelButton, 'Units', 'normalized'); +set(confirmButton, 'Units', 'normalized'); +set(setCheckboxes, 'Units', 'normalized'); +set(startOffsetFields, 'Units', 'normalized'); +set(endOffsetFields, 'Units', 'normalized'); + +% set widget callbacks +set(f, 'CloseRequestFcn', @cancelCallback); +set(f, 'WindowKeyPressFcn', @keyPressCallback); +set(setCheckboxes, 'Callback', @checkboxCallback); +set(startOffsetFields, 'Callback', @startoffsetFieldCallback); +set(endOffsetFields, 'Callback', @endoffsetFieldCallback); +set(cancelButton, 'Callback', @cancelCallback); +set(confirmButton, 'Callback', @confirmCallback); + +set(f, 'Visible', 'on'); + +uiwait(f); + +% calculate the drift and apply to the selected datasets +for k = 1:nSample + + % this set has been deselected + if ~sets(k), continue; end + + %check for zero values in both fields + if startOffsets(k) == 0 && endOffsets(k) == 0 + continue + end + + % look time through dimensions + type = 'dimensions'; + timeIdx = getVar(sample_data{k}.(type), 'TIME'); + + if timeIdx == 0 + % look time through variables + type = 'variables'; + timeIdx = getVar(sample_data{k}.(type), 'TIME'); + end + + % no time dimension nor variable in this dataset + if timeIdx == 0, continue; end + + timeDriftComment = ['timeDriftPP: TIME values and time_coverage_end global attributes have been have been '... + 'linearly adjusted for an offset of: ' num2str(startOffsets(k)) ' seconds and a drift of: ' num2str(endOffsets(k) - startOffsets(k)) ' seconds ' ... + 'across the deployment.']; + + % apply the drift correction + newtime = timedrift_corr(sample_data{k}.(type){timeIdx}.data, ... + startOffsets(k),endOffsets(k)); + sample_data{k}.(type){timeIdx}.data = newtime; + + % and to the time coverage atttributes + sample_data{k}.time_coverage_start = newtime(1); + sample_data{k}.time_coverage_end = ... + newtime(end); + + + comment = sample_data{k}.(type){timeIdx}.comment; + if isempty(comment) + sample_data{k}.(type){timeIdx}.comment = timeDriftComment; + else + sample_data{k}.(type){timeIdx}.comment = [comment ' ' timeDriftComment]; + end + + history = sample_data{k}.history; + if isempty(history) + sample_data{k}.history = sprintf('%s - %s', datestr(now_utc, ... + readProperty('exportNetCDF.dateFormat')), timeDriftComment); + else + sample_data{k}.history = sprintf('%s\n%s - %s', history, ... + datestr(now_utc, readProperty('exportNetCDF.dateFormat')), timeDriftComment); + end +end + + function keyPressCallback(source,ev) + %KEYPRESSCALLBACK If the user pushes escape/return while the dialog has + % focus, the dialog is cancelled/confirmed. This is done by delegating + % to the cancelCallback/confirmCallback functions. + % + if strcmp(ev.Key, 'escape'), cancelCallback( source,ev); + elseif strcmp(ev.Key, 'return'), confirmCallback(source,ev); + end + end + + function cancelCallback(source,ev) + %CANCELCALLBACK Cancel button callback. Discards user input and closes the + % dialog . + % + sets(:) = 0; + startOffsets(:) = 0; + delete(f); + end + + function confirmCallback(source,ev) + %CONFIRMCALLBACK. Confirm button callback. Closes the dialog. + % + delete(f); + end + + function checkboxCallback(source, ev) + %CHECKBOXCALLBACK Called when a checkbox selection is changed. + % Enables/disables the offset text field. + % + idx = get(source, 'UserData'); + val = get(source, 'Value'); + + sets(idx) = val; + + if val, val = 'on'; + else val = 'off'; + end + + set(startOffsetFields(idx), 'Enable', val); + + end + + function startoffsetFieldCallback(source, ev) + %OFFSETFIELDCALLBACK Called when the user edits one of the offset fields. + % Verifies that the text entered is a number. + % + + val = get(source, 'String'); + idx = get(source, 'UserData'); + + val = str2double(val); + + % reset the offset value on non-numerical + % input, otherwise save the new value + if isnan(val), set(source, 'String', num2str(startOffsets(idx))); + else startOffsets(idx) = val; + + end + end + + function endoffsetFieldCallback(source, ev) + %OFFSETFIELDCALLBACK Called when the user edits one of the offset fields. + % Verifies that the text entered is a number. + % + + val = get(source, 'String'); + idx = get(source, 'UserData'); + + val = str2double(val); + + % reset the offset value on non-numerical + % input, otherwise save the new value + if isnan(val), set(source, 'String', num2str(endOffsets(idx))); + else endOffsets(idx) = val; + + end + end + + function newtime = timedrift_corr(time,offset_s,offset_e) + %remove linear drift of time (in days) from any instrument. + %the drift is calculated using the start offset (offset_s in seconds) and the + % end offset (offset_e in seconds). + %Bec Cowley, April 2014 + + % change the offset times to days: + offset_e = offset_e/60/60/24; + offset_s = offset_s/60/60/24; + + %make an array of time corrections using the offsets: + tarray = (offset_s:(offset_e-offset_s)/(length(time)-1):offset_e)'; + if isempty(tarray) + newtime = []; + else + newtime = time - tarray; + end + end +end \ No newline at end of file From 1221f3fee5dad0631df8c6cc188b68d8f831a777 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Mon, 4 Apr 2016 11:05:42 +1000 Subject: [PATCH 09/21] Fix rinkoDoPP correction from pressure in MPascal and not dBar. --- Preprocessing/rinkoDoPP.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Preprocessing/rinkoDoPP.m b/Preprocessing/rinkoDoPP.m index f41837a54..540349d4d 100644 --- a/Preprocessing/rinkoDoPP.m +++ b/Preprocessing/rinkoDoPP.m @@ -157,7 +157,7 @@ DO = G + H*DO; % correction for pressure - DO = DO.*(1 + E*presRel); % DO is in % of dissolved oxygen during calibration at this stage + DO = DO.*(1 + E*presRel/100); % pressRel/100 => conversion dBar to MPa (see rinko correction formula pdf). DO is in % of dissolved oxygen during calibration at this stage. if psalIdx > 0 psal = sam.variables{psalIdx}.data; From d1ef006b8461a1ba2e9a12935be94a553d94a002 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Tue, 5 Apr 2016 14:50:19 +1000 Subject: [PATCH 10/21] Files .pqc and .mqc are excluded when looking for matching files on disk from radical names in ddb. --- FlowManager/importManager.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/FlowManager/importManager.m b/FlowManager/importManager.m index e32857bab..5358357e6 100644 --- a/FlowManager/importManager.m +++ b/FlowManager/importManager.m @@ -305,7 +305,15 @@ hits = fsearch(rawFile, dataDir, 'files'); - allFiles{k} = hits; + % we remove any potential .pqc or .mqc files found (reserved for use + % by the toolbox) + reservedExts = {'.pqc', '.mqc'}; + for l=1:length(hits) + [~, ~, ext] = fileparts(hits{l}); + if all(~strcmp(ext, reservedExts)) + allFiles{k}{end+1} = hits{l}; + end + end end % display status dialog to highlight any discrepancies (file not found From 5a282a2a8e358fb1f760122fd984390196dddaa0 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Wed, 6 Apr 2016 15:17:22 +1000 Subject: [PATCH 11/21] Improve fsearch to only include files/directories with a strict same radical name. --- Util/fsearch.m | 28 +++++++++++++++++++++++++--- Util/imosCompile.m | 2 +- Util/imosPackage.m | 2 +- imosToolbox.m | 2 +- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Util/fsearch.m b/Util/fsearch.m index 22c193149..60e038428 100644 --- a/Util/fsearch.m +++ b/Util/fsearch.m @@ -89,7 +89,14 @@ case 'dirs' if ~d.isdir, continue; end if ~isempty(strfind(lower(fullfile(root, d.name)), lower(pattern))) - hits{end+1} = fullfile(root, d.name); + % we check that the radical name is the one we're + % looking for. We want 4T3993 to match path/4T3993/ but not + % path/454T3993 + [~, foundName, foundExt] = fileparts(d.name); + [~, patternName, patternExt] = fileparts(pattern); + if strcmpi(foundName, patternName) + hits{end+1} = fullfile(root, d.name); + end end case 'files' if d.isdir @@ -98,12 +105,27 @@ hits = [hits subhits]; else if ~isempty(strfind(lower(fullfile(root, d.name)), lower(pattern))) - hits{end+1} = fullfile(root, d.name); + % we check that the radical name is the one we're + % looking for. We want 4T3993 to match 4T3993.DAT or + % 4T3993.csv but not 454T3993.DAT + [~, foundName, foundExt] = fileparts(d.name); + [~, patternName, patternExt] = fileparts(pattern); + if strcmpi(foundName, patternName) + hits{end+1} = fullfile(root, d.name); + end end end otherwise % both if ~isempty(strfind(lower(fullfile(root, d.name)), lower(pattern))) - hits{end+1} = fullfile(root, d.name); + % we check that the radical name is the one we're + % looking for. We want 4T3993 to match path/4T3993/ or + % path/4T3993.DAT but not path/454T3993 nor + % path/454T3993.DAT + [~, foundName, foundExt] = fileparts(d.name); + [~, patternName, patternExt] = fileparts(pattern); + if strcmpi(foundName, patternName) + hits{end+1} = fullfile(root, d.name); + end end if d.isdir diff --git a/Util/imosCompile.m b/Util/imosCompile.m index 02413d601..5cb2b2b78 100644 --- a/Util/imosCompile.m +++ b/Util/imosCompile.m @@ -70,7 +70,7 @@ function imosCompile() % find all .m and .mat files - these are to be % included as resources in the standalone application matlabFiles = fsearchRegexp('.m$', exportRoot, 'files'); -matlabDataFiles = fsearch('.mat', exportRoot, 'files'); +matlabDataFiles = fsearchRegexp('.mat$', exportRoot, 'files'); % we leave out imosToolbox.m because, as the main % function, it must be listed first. diff --git a/Util/imosPackage.m b/Util/imosPackage.m index cec8dd3bb..261885cb3 100644 --- a/Util/imosPackage.m +++ b/Util/imosPackage.m @@ -83,7 +83,7 @@ function imosPackage() % find all .m, .mat, .txt, .jar, .bat, .exe, .bin and .sh files - these are to be % included as resources in the standalone application matlabFiles = fsearchRegexp('.m$', toolboxRoot, 'files'); -matlabDataFiles = fsearch('.mat', toolboxRoot, 'files'); +matlabDataFiles = fsearchRegexp('.mat$', toolboxRoot, 'files'); resourceFiles = [matlabFiles matlabDataFiles]; resourceFiles = [resourceFiles fsearchRegexp('.txt$', toolboxRoot, 'files')]; resourceFiles = [resourceFiles fsearchRegexp('.COF$', toolboxRoot, 'files')]; diff --git a/imosToolbox.m b/imosToolbox.m index ea266c816..37e970d6c 100644 --- a/imosToolbox.m +++ b/imosToolbox.m @@ -67,7 +67,7 @@ function imosToolbox(auto, varargin) % we must dynamically add the ddb.jar java library to the classpath % as well as any other .jar library and jdbc drivers -jars = fsearch('.jar', fullfile(path, 'Java'), 'files'); +jars = fsearchRegexp('.jar$', fullfile(path, 'Java'), 'files'); for j = 1 : length(jars) javaaddpath(jars{j}); end From ffb91df39a0d34c0e22c5352feaf91b04833e731 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Wed, 6 Apr 2016 15:31:45 +1000 Subject: [PATCH 12/21] Added support to deployment database in CSV files. --- DDB/executeCSVQuery.m | 184 +++++++++++++++++++++++++++++++++++ FlowManager/importManager.m | 42 +++++--- GUI/dataFileStatusDialog.m | 9 +- GUI/startDialog.m | 15 ++- NetCDF/makeNetCDFCompliant.m | 15 +-- Util/getDeployments.m | 34 +++++-- Util/parseAttributeValue.m | 10 +- 7 files changed, 275 insertions(+), 34 deletions(-) create mode 100644 DDB/executeCSVQuery.m diff --git a/DDB/executeCSVQuery.m b/DDB/executeCSVQuery.m new file mode 100644 index 000000000..f8496c325 --- /dev/null +++ b/DDB/executeCSVQuery.m @@ -0,0 +1,184 @@ +function result = executeCSVQuery( file, field, value) +%EXECUTECSVQUERY Alternative to executeDDBQuery, uses CSV files. +% +% Uses multiple csv files to obtain information equivalent to +% executeDDBQuery. +% +% Inputs: +% file - The csv file to query. +% +% field - Name of field to search for value. If passed in as an +% empty matrix, the entire table is returned. +% +% value - Value of field on which to restrict query. +% +% Outputs: +% result - Vector of structs, each entry representing one tuple of the +% query result. +% +% Author: Rebecca Cowley + +% +% Copyright (c) 2009, eMarine Information Infrastructure (eMII) and Integrated +% Marine Observing System (IMOS). +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright +% notice, this list of conditions and the following disclaimer in the +% documentation and/or other materials provided with the distribution. +% * Neither the name of the eMII/IMOS nor the names of its contributors +% may be used to endorse or promote products derived from this software +% without specific prior written permission. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% + narginchk(3,3); + + if ~ischar(file), error('file must be a string'); end + if ~isempty(field) ... + && ~ischar(field), error('field must be a string'); end + + % in order to reduce the number of queries to the ddb (slow, we store + % each result in a global structure so that we don't perform twice the + % same query. + persistent csvStruct; + if isempty(csvStruct) + csvStruct = struct; + csvStruct.table = {}; + csvStruct.field = {}; + csvStruct.value = {}; + csvStruct.result = {}; + else + iTable = strcmpi(file, csvStruct.table); + if any(iTable) + iField = strcmpi(field, csvStruct.field); + iField = iField & iTable; + if any(iField) + iValue = strcmpi(csvStruct.value, num2str(value)); + iValue = iValue & iField; + if any(iValue) + result = csvStruct.result{iValue}; + return; + end + end + end + end + + %get location of csv files: + dirnm = readProperty('toolbox.ddb.connection'); + if isempty(dirnm) + dirnm = pwd; + end + + % complete the file name: + file = fullfile(dirnm, [file '.csv']); + + %check the file exists, if not, prompt user to select file + if exist(file,'file') == 0 + %open dialog to select a file + disp(['Deployment CSV file ' file ' not found']) + return + % Code it in.... + end + + % open the file + fid = fopen(file); + + %figure out how many columns we have: + header1 = fgetl(fid); + ncols = length(strfind(header1,',')) + 1; + fmtHeader1 = repmat('%q', 1, ncols); + header1 = textscan(header1, fmtHeader1, 'Delimiter', ',', 'CollectOutput', 1); + header1 = header1{1}; + + %build the format string + header2 = fgetl(fid); + fmt = strrep(header2, ',', ''); + + %close and re-open the file + fclose(fid); + fid = fopen(file); + + %extract all the data + data = textscan(fid, fmt, ... + 'Delimiter', ',' , ... + 'HeaderLines', 2); +% data = data{1}; + for i=1:length(data) + if isfloat(data{i}) + myData(:, i) = num2cell(data{i}); + else + myData(:, i) = data{i}; + end + end + fclose(fid); + + result = extractdata(header1, myData, field, value); + + % save result in structure + csvStruct.table{end+1} = file; + csvStruct.field{end+1} = field; + csvStruct.value{end+1} = num2str(value); + csvStruct.result{end+1} = result; +end + +function result = extractdata(header, data, field, value); +% Extract the required values from the data +%headers become field names + +if ~isempty(field) + ifield = strcmp(field,header); + if ~any(ifield) + disp(['Field ' field ' not found in the csv file']) + result = []; + return + end + + % extract the values in field: + ivalue = strcmp(value,data(:,ifield)); + data = data(ivalue,:); +end + +%look for dates and convert to matlab date format: +dateTimeFmt = 'dd/mm/yy HH:MM'; +idate = cellfun(@isempty,strfind(lower(header),'date')); +idate = find(~idate); +for a = idate + iempty = cellfun(@isempty, data(:,a)); + data(~iempty,a) = cellfun(@(x) datenum(x,dateTimeFmt), data(~iempty,a), 'Uniform', false); +% iempty = cellfun(@isempty, data(:,a)); +% cellDateStr = data{~iempty,a}; +% arrayDateNum = datenum(cellDateStr, dateTimeFmt); +% cellDateNum = num2cell(arrayDateNum); +% data{~iempty,a} = cellDateNum; +end + +%look for times and convert to matlab date format: +idate = cellfun(@isempty,strfind(lower(header),'time')); +idateTZ = cellfun(@isempty,strfind(lower(header),'timezone')); +idateTD = cellfun(@isempty,strfind(lower(header),'timedriftinstrument')); +idate = find(~idate & idateTZ & idateTD); +for a = idate + iempty = cellfun(@isempty, data(:,a)); + data(~iempty,a) = cellfun(@(x) datenum(x,dateTimeFmt), data(~iempty,a), 'Uniform', false); +end + +%make the structure: +result = cell2struct(data,header,2); + +end diff --git a/FlowManager/importManager.m b/FlowManager/importManager.m index 5358357e6..3a36969d1 100644 --- a/FlowManager/importManager.m +++ b/FlowManager/importManager.m @@ -71,7 +71,8 @@ end % If the toolbox.ddb property has been set, assume that we have a - % deployment database. Otherwise perform a manual import + % deployment database. Or if it is designated as 'csv', use a CSV file for + % import. Otherwise perform a manual import. ddb = readProperty('toolbox.ddb'); driver = readProperty('toolbox.ddb.driver'); @@ -81,10 +82,10 @@ rawFiles = {}; if ~isempty(ddb) || (~isempty(driver) && ~isempty(connection)) - [structs rawFiles] = ddbImport(auto, iMooring); + [structs rawFiles] = ddbImport(auto, iMooring, ddb); else - if auto, error('manual import cannot be automated without deployment database'); end - [structs rawFiles] = manualImport(); + if auto, error('manual import cannot be automated without deployment database'); end + [structs rawFiles] = manualImport(); end % user cancelled @@ -190,13 +191,18 @@ end end -function [sample_data rawFiles] = ddbImport(auto, iMooring) +function [sample_data rawFiles] = ddbImport(auto, iMooring, ddb) %DDBIMPORT Imports data sets using metadata retrieved from a deployment % database. % % Inputs: % auto - if true, the import process is automated, with no user % interaction. +% iMooring - Optional logical(comes with auto == true). Contains +% the logical indices to extract only the deployments +% from one mooring set of deployments. +% ddb - deployment database string attribute from +% toolboxProperties.txt % % Outputs: % sample_data - cell array containig the imported data sets, or empty @@ -304,7 +310,7 @@ rawFile = deps(k).FileName; hits = fsearch(rawFile, dataDir, 'files'); - + % we remove any potential .pqc or .mqc files found (reserved for use % by the toolbox) reservedExts = {'.pqc', '.mqc'}; @@ -356,9 +362,8 @@ fileDisplay = fileDisplay(3:end); waitbar(k / length(deps), progress, fileDisplay); end - % import data - sample_data{end+1} = parse(deps(k), allFiles{k}, parsers, noParserPrompt, mode); + sample_data{end+1} = parse(deps(k), allFiles{k}, parsers, noParserPrompt, mode, ddb); rawFiles{ end+1} = allFiles{k}; if iscell(sample_data{end}) @@ -415,7 +420,7 @@ % close progress dialog if ~auto, close(progress); end - function sam = parse(deployment, files, parsers, noParserPrompt, mode) + function sam = parse(deployment, files, parsers, noParserPrompt, mode, ddb) %PARSE Parses a raw data file, returns a sample_data struct. % % Inputs: @@ -424,12 +429,14 @@ % parsers - Cell array of strings containing all available parsers. % noParserPrompt - Whether to prompt the user if a parser cannot be found. % mode - Toolbox data type mode ('profile' or 'timeSeries'). + % ddb - deployment database string attribute from + % toolboxProperties.txt % % Outputs: % sam - Struct containing sample data. % get the appropriate parser function - parser = getParserFunc(deployment, parsers, noParserPrompt); + parser = getParserFunc(deployment, parsers, noParserPrompt, ddb); if isnumeric(parser) error(['no parser found for instrument ' deployment.InstrumentID]); end @@ -438,7 +445,7 @@ sam = parser(files, mode); end - function parser = getParserFunc(deployment, parsers, noParserPrompt) + function parser = getParserFunc(deployment, parsers, noParserPrompt, ddb) %GETPARSERFUNC Searches for a parser function which is able to parse data % for the given deployment. % @@ -446,13 +453,20 @@ % deployment - struct containing information about the deployment. % parsers - Cell array of strings containing all available parsers. % noParserPrompt - Whether to prompt the user if a parser cannot be found. + % ddb - deployment database string attribute from + % toolboxProperties.txt % % Outputs: % parser - Function handle to the parser function, or 0 if a parser % function wasn't found. % - instrument = executeDDBQuery(... - 'Instruments', 'InstrumentID', deployment.InstrumentID); + if strcmp('csv',ddb) + instrument = executeCSVQuery(... + 'Instruments', 'InstrumentID', deployment.InstrumentID); + else + instrument = executeDDBQuery(... + 'Instruments', 'InstrumentID', deployment.InstrumentID); + end % there should be exactly one instrument if length(instrument) ~= 1 @@ -488,4 +502,4 @@ % get the parser function handle parser = getParser(parser); end -end \ No newline at end of file +end diff --git a/GUI/dataFileStatusDialog.m b/GUI/dataFileStatusDialog.m index ce36ab450..d18ba1086 100644 --- a/GUI/dataFileStatusDialog.m +++ b/GUI/dataFileStatusDialog.m @@ -305,8 +305,9 @@ function fileAddCallback(source,ev) % deployments, suitable for use in the deployments list. % + ddb = readProperty('toolbox.ddb'); + % set values for lists - descs = {}; for k = 1:length(deployments) @@ -326,7 +327,11 @@ function fileAddCallback(source,ev) % get some site information if it exists - site = executeDDBQuery('Sites', 'Site', dep.Site); + if strcmp('csv', ddb) + site = executeCSVQuery('Sites', 'Site', dep.Site); + else + site = executeDDBQuery('Sites', 'Site', dep.Site); + end if ~isempty(site) diff --git a/GUI/startDialog.m b/GUI/startDialog.m index 1b0d27f6b..0b9817b55 100644 --- a/GUI/startDialog.m +++ b/GUI/startDialog.m @@ -1,4 +1,4 @@ -function [fieldTrip dataDir] = startDialog(mode) +function [fieldTrip dataDir] = startDialog(mode, isCSV) %STARTDIALOG Displays a dialog prompting the user to select a Field Trip % and a directory which contains raw data files. % @@ -11,6 +11,7 @@ % Input: % % mode - String, toolox execution mode can be 'profile' or 'timeSeries'. +% isCSV - optional boolean (default = false). True if importing from csv files. % % Outputs: % @@ -52,7 +53,11 @@ % ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE % POSSIBILITY OF SUCH DAMAGE. % - narginchk(1,1); + narginchk(1,2); + + if nargin == 1 + isCSV = false; + end dateFmt = readProperty('toolbox.dateFormat'); @@ -77,7 +82,11 @@ if isnan(highDate), highDate = now_utc; end % retrieve all field trip IDs; they are displayed as a drop down menu - fieldTrips = executeDDBQuery('FieldTrip', [], []); + if isCSV + fieldTrips = executeCSVQuery('FieldTrip', [], []); + else + fieldTrips = executeDDBQuery('FieldTrip', [], []); + end if isempty(fieldTrips), error('No field trip entries in DDB'); end diff --git a/NetCDF/makeNetCDFCompliant.m b/NetCDF/makeNetCDFCompliant.m index 2d47cdb84..de58c8cfb 100644 --- a/NetCDF/makeNetCDFCompliant.m +++ b/NetCDF/makeNetCDFCompliant.m @@ -151,7 +151,7 @@ % % variables % - + ddb = readProperty('toolbox.ddb'); for k = 1:length(sample_data.variables) @@ -173,11 +173,11 @@ if isfield(sample_data.meta, 'deployment') iTime = getVar(sample_data.dimensions, 'TIME'); sample_data.variables{k}.sensor_serial_number = ... - getSensorSerialNumber(sample_data.variables{k}.name, sample_data.meta.deployment.InstrumentID, sample_data.dimensions{iTime}.data(1)); + getSensorSerialNumber(sample_data.variables{k}.name, sample_data.meta.deployment.InstrumentID, sample_data.dimensions{iTime}.data(1), ddb); elseif isfield(sample_data.meta, 'profile') iTime = getVar(sample_data.variables, 'TIME'); sample_data.variables{k}.sensor_serial_number = ... - getSensorSerialNumber(sample_data.variables{k}.name, sample_data.meta.profile.InstrumentID, sample_data.variables{iTime}.data(1)); + getSensorSerialNumber(sample_data.variables{k}.name, sample_data.meta.profile.InstrumentID, sample_data.variables{iTime}.data(1), ddb); end end end @@ -200,7 +200,7 @@ end end -function target = getSensorSerialNumber ( IMOSParam, InstrumentID, timeFirstSample ) +function target = getSensorSerialNumber ( IMOSParam, InstrumentID, timeFirstSample, ddb ) %GETSENSORSERIALNUMBER gets the sensor serial number associated to an IMOS %paramter for a given deployment ID % @@ -208,8 +208,11 @@ target = ''; % query the ddb for all sensor config related to this instrument ID -InstrumentSensorConfig = executeDDBQuery('InstrumentSensorConfig', 'InstrumentID', InstrumentID); - +if strcmp('csv',ddb) + InstrumentSensorConfig = executeCSVQuery('InstrumentSensorConfig', 'InstrumentID', InstrumentID); +else + InstrumentSensorConfig = executeDDBQuery('InstrumentSensorConfig', 'InstrumentID', InstrumentID); +end lenConfig = length(InstrumentSensorConfig); % only consider relevant config based on timeFirstSample for i=1:lenConfig diff --git a/Util/getDeployments.m b/Util/getDeployments.m index 040d62911..dfabee25e 100644 --- a/Util/getDeployments.m +++ b/Util/getDeployments.m @@ -1,4 +1,4 @@ -function [fieldTrip deployments sites dataDir] = getDeployments(auto) +function [fieldTrip deployments sites dataDir] = getDeployments(auto, isCSV) %GETDEPLOYMENTS Prompts the user for a field trip ID and data directory. % Retrieves and returns the field trip, all deployments from the DDB that % are related to the field trip, and the selected data directory. @@ -7,6 +7,8 @@ % auto - if true, the user is not prompted to select a field % trip/directory; the values in toolboxProperties are % used. +% isCSV - optional [false = default]. If true, look for csv files +% rather than using database % % Outputs: % fieldTrip - field trip struct - the field trip selected by the user. @@ -49,13 +51,25 @@ % ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE % POSSIBILITY OF SUCH DAMAGE. % +if nargin == 1 + isCSV = false; +end + deployments = struct; sites = struct; +%check for CSV file import: +ddb = readProperty('toolbox.ddb'); +if strcmp(ddb,'csv') + isCSV = true; +else + isCSV = false; +end + % prompt the user to select a field trip and % directory which contains raw data files if ~auto - [fieldTrip dataDir] = startDialog('timeSeries'); + [fieldTrip, dataDir] = startDialog('timeSeries', isCSV); % if automatic, just get the defaults from toolboxProperties.txt else dataDir = readProperty('startDialog.dataDir.timeSeries'); @@ -72,15 +86,21 @@ fId = fieldTrip.FieldTripID; -% query the ddb for all deployments related to this field trip -deployments = executeDDBQuery('DeploymentData', 'EndFieldTrip', fId); +if isCSV + executeQueryFunc = @executeCSVQuery; +else + executeQueryFunc = @executeDDBQuery; +end + +% query the ddb/csv file for all deployments related to this field trip +deployments = executeQueryFunc('DeploymentData', 'EndFieldTrip', fId); % query the ddb for all sites related to these deployments lenDep = length(deployments); for i=1:lenDep if i==1 - sites = executeDDBQuery('Sites', 'Site', deployments(i).Site); + sites = executeQueryFunc('Sites', 'Site', deployments(i).Site); else - sites(i) = executeDDBQuery('Sites', 'Site', deployments(i).Site); + sites(i) = executeQueryFunc('Sites', 'Site', deployments(i).Site); end -end \ No newline at end of file +end diff --git a/Util/parseAttributeValue.m b/Util/parseAttributeValue.m index 93da212c1..7716dd6a0 100644 --- a/Util/parseAttributeValue.m +++ b/Util/parseAttributeValue.m @@ -163,7 +163,9 @@ % if there is no deployment database, set the value to an empty matrix ddb = readProperty('toolbox.ddb'); - if isempty(ddb), return; end + driver = readProperty('toolbox.ddb.driver'); + connection = readProperty('toolbox.ddb.connection'); + if isempty(ddb) && (isempty(driver) || isempty(connection)), return; end % get the relevant deployment/CTD cast if isfield(sample_data.meta, 'profile') @@ -206,7 +208,11 @@ % a foreign key, so our only choice is to give up if isempty(field_value), return; end - result = executeDDBQuery(related_table, related_pkey, field_value); + if strcmp('csv',ddb) + result = executeCSVQuery(related_table, related_pkey, field_value); + else + result = executeDDBQuery(related_table, related_pkey, field_value); + end if length(result) ~= 1, return; end value = result.(related_field); From 5d0de7912897f2fe4cbfca016bc106c311f639af Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Mon, 11 Apr 2016 15:07:02 +1000 Subject: [PATCH 13/21] Fix AD2CP readClockData function. --- Parser/readAD2CPBinary.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Parser/readAD2CPBinary.m b/Parser/readAD2CPBinary.m index 98c2e902a..f35e7de0c 100644 --- a/Parser/readAD2CPBinary.m +++ b/Parser/readAD2CPBinary.m @@ -175,8 +175,8 @@ data = data(idx:idx+7); -year = bytecast(data(1), 'L', 'uint8', cpuEndianness); -month = bytecast(data(2), 'L', 'uint8', cpuEndianness); +year = bytecast(data(1), 'L', 'uint8', cpuEndianness) + 1900; % years since 1900 +month = bytecast(data(2), 'L', 'uint8', cpuEndianness) + 1; % Jan=0, Feb=1, etc. day = bytecast(data(3), 'L', 'uint8', cpuEndianness); hour = bytecast(data(4), 'L', 'uint8', cpuEndianness); minute = bytecast(data(5), 'L', 'uint8', cpuEndianness); @@ -184,9 +184,9 @@ hundredsusecond = bytecast(data(7:8), 'L', 'uint16', cpuEndianness); second = second + hundredsusecond/10000; -year = year + 1900; cd = datenummx(year, month, day, hour, minute, second); % direct access to MEX function, faster + end function [header, len, off] = readHeader(data, idx, cpuEndianness) From d93e23dbd05fe1ad28a514e567154884cbd4e723 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Mon, 11 Apr 2016 15:10:15 +1000 Subject: [PATCH 14/21] Add variable applied_offset with Seabird pressure offset for PRES_REL read by SBE3x.m. --- Parser/SBE19Parse.m | 2 +- Parser/SBE3x.m | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Parser/SBE19Parse.m b/Parser/SBE19Parse.m index acb5615e3..ef3b1d369 100644 --- a/Parser/SBE19Parse.m +++ b/Parser/SBE19Parse.m @@ -331,7 +331,7 @@ sample_data.variables{end}.coordinates = 'TIME LATITUDE LONGITUDE DEPTH'; end - if strcmpi('PRES_REL', vars{k}) + if strncmp('PRES_REL', vars{k}, 8) % let's document the constant pressure atmosphere offset previously % applied by SeaBird software on the absolute presure measurement sample_data.variables{end}.applied_offset = sample_data.variables{end}.typeCastFunc(-14.7*0.689476); diff --git a/Parser/SBE3x.m b/Parser/SBE3x.m index 014e07a40..443c43144 100644 --- a/Parser/SBE3x.m +++ b/Parser/SBE3x.m @@ -483,7 +483,11 @@ switch varNames{k} case TEMPERATURE_NAME, sample_data.variables{end}.data = sample_data.variables{end}.typeCastFunc(temperature); case CONDUCTIVITY_NAME, sample_data.variables{end}.data = sample_data.variables{end}.typeCastFunc(conductivity); - case PRESSURE_NAME, sample_data.variables{end}.data = sample_data.variables{end}.typeCastFunc(pressure); + case PRESSURE_NAME + sample_data.variables{end}.data = sample_data.variables{end}.typeCastFunc(pressure); + % let's document the constant pressure atmosphere offset previously + % applied by SeaBird software on the absolute presure measurement + sample_data.variables{end}.applied_offset = sample_data.variables{end}.typeCastFunc(-14.7*0.689476); case SALINITY_NAME, sample_data.variables{end}.data = sample_data.variables{end}.typeCastFunc(salinity); end end From 48551ab73a0b9f5578991fd93fcc0d4fb7d5c9c0 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Mon, 11 Apr 2016 15:51:26 +1000 Subject: [PATCH 15/21] Added checking plots on pressure (actual vs nominal depth, and comparison with neighbours for sensor drift). --- GUI/mainWindow.m | 51 ++++- Graph/checkMooringPlannedDepths.m | 293 ++++++++++++++++++++++++++ Graph/checkMooringPresDiffs.m | 329 ++++++++++++++++++++++++++++++ Util/select_points.m | 20 ++ 4 files changed, 685 insertions(+), 8 deletions(-) create mode 100644 Graph/checkMooringPlannedDepths.m create mode 100644 Graph/checkMooringPresDiffs.m create mode 100644 Util/select_points.m diff --git a/GUI/mainWindow.m b/GUI/mainWindow.m index 4a4863959..154cb4121 100644 --- a/GUI/mainWindow.m +++ b/GUI/mainWindow.m @@ -277,6 +277,12 @@ function mainWindow(... %set uimenu hToolsMenu = uimenu(fig, 'label', 'Tools'); if strcmpi(mode, 'timeseries') + hToolsCheckPlannedDepths = uimenu(hToolsMenu, 'label', 'Check measured against planned depths'); + hToolsCheckPlannedDepthsNonQC = uimenu(hToolsCheckPlannedDepths, 'label', 'non QC'); + hToolsCheckPlannedDepthsQC = uimenu(hToolsCheckPlannedDepths, 'label', 'QC'); + hToolsCheckPressDiffs = uimenu(hToolsMenu, 'label', 'Check pressure differences between selected instrument and nearest neighbours'); + hToolsCheckPressDiffsNonQC = uimenu(hToolsCheckPressDiffs, 'label', 'non QC'); + hToolsCheckPressDiffsQC = uimenu(hToolsCheckPressDiffs, 'label', 'QC'); hToolsLineDepth = uimenu(hToolsMenu, 'label', 'Line plot mooring''s depths'); hToolsLineDepthNonQC = uimenu(hToolsLineDepth, 'label', 'non QC'); hToolsLineDepthQC = uimenu(hToolsLineDepth, 'label', 'QC'); @@ -291,14 +297,18 @@ function mainWindow(... hToolsScatter2DCommonVarQC = uimenu(hToolsScatter2DCommonVar, 'label', 'QC'); %set menu callbacks - set(hToolsLineDepthNonQC, 'callBack', {@displayLineMooringDepth, false}); - set(hToolsLineDepthQC, 'callBack', {@displayLineMooringDepth, true}); - set(hToolsLineCommonVarNonQC, 'callBack', {@displayLineMooringVar, false}); - set(hToolsLineCommonVarQC, 'callBack', {@displayLineMooringVar, true}); - set(hToolsScatterCommonVarNonQC, 'callBack', {@displayScatterMooringVar, false, true}); - set(hToolsScatterCommonVarQC, 'callBack', {@displayScatterMooringVar, true, true}); - set(hToolsScatter2DCommonVarNonQC,'callBack', {@displayScatterMooringVar, false, false}); - set(hToolsScatter2DCommonVarQC, 'callBack', {@displayScatterMooringVar, true, false}); + set(hToolsCheckPlannedDepthsNonQC, 'callBack', {@displayCheckPlannedDepths, false}); + set(hToolsCheckPlannedDepthsQC, 'callBack', {@displayCheckPlannedDepths, true}); + set(hToolsCheckPressDiffsNonQC, 'callBack', {@displayCheckPressDiffs, false}); + set(hToolsCheckPressDiffsQC, 'callBack', {@displayCheckPressDiffs, true}); + set(hToolsLineDepthNonQC, 'callBack', {@displayLineMooringDepth, false}); + set(hToolsLineDepthQC, 'callBack', {@displayLineMooringDepth, true}); + set(hToolsLineCommonVarNonQC, 'callBack', {@displayLineMooringVar, false}); + set(hToolsLineCommonVarQC, 'callBack', {@displayLineMooringVar, true}); + set(hToolsScatterCommonVarNonQC, 'callBack', {@displayScatterMooringVar, false, true}); + set(hToolsScatterCommonVarQC, 'callBack', {@displayScatterMooringVar, true, true}); + set(hToolsScatter2DCommonVarNonQC, 'callBack', {@displayScatterMooringVar, false, false}); + set(hToolsScatter2DCommonVarQC, 'callBack', {@displayScatterMooringVar, true, false}); else hToolsLineCastVar = uimenu(hToolsMenu, 'label', 'Line plot profile variables'); hToolsLineCastVarNonQC = uimenu(hToolsLineCastVar, 'label', 'non QC'); @@ -467,6 +477,31 @@ function zoomPostCallback(source,ev) end %% Menu callback + function displayCheckPressDiffs(source,ev, isQC) + %DISPLAYLINEPRESSDIFFS opens a new window where all the PRES/PRES_REL + %values for instruments adjacent to the current instrument are + %displayed with the differences between these instrument pressures + % + %check for pressure + iSampleMenu = get(sampleMenu, 'Value'); + iPRES_REL = getVar(sample_data{iSampleMenu}.variables, 'PRES_REL'); + iPRES = getVar(sample_data{iSampleMenu}.variables, 'PRES'); + if iPRES_REL == 0 && iPRES == 0 + sampleMenuStrings = get(sampleMenu, 'String'); + disp(['No pressure data for ' sampleMenuStrings{iSampleMenu}]) + return + end + + checkMooringPresDiffs(sample_data, iSampleMenu, isQC, false, ''); + end + + function displayCheckPlannedDepths(source,ev, isQC) + %DISPLAYCHECKPLANNEDDEPTHS Opens a new window where the actual + %depths recorded are compared to the planned depths. + % + checkMooringPlannedDepths(sample_data, isQC, false, ''); + end + function displayLineMooringDepth(source,ev, isQC) %DISPLAYLINEMOORINGDEPTH Opens a new window where all the nominal depths and %actual/computed depths from intruments on the mooring are line-plotted. diff --git a/Graph/checkMooringPlannedDepths.m b/Graph/checkMooringPlannedDepths.m new file mode 100644 index 000000000..88c0807b1 --- /dev/null +++ b/Graph/checkMooringPlannedDepths.m @@ -0,0 +1,293 @@ +function checkMooringPlannedDepths(sample_data, isQC, saveToFile, exportDir) +%CHECKMOORINGPLANNEDDEPTHS Opens a new window where the DEPTH +% variable of all intruments is plotted and compared to the +% planned depth. +% +% Inputs: +% sample_data - cell array of structs containing the entire data set and dimension data. +% +% varName - string containing the IMOS code for requested parameter. +% +% isQC - logical to plot only good data or not. +% +% saveToFile - logical to save the plot on disk or not. +% +% exportDir - string containing the destination folder to where the +% plot is saved on disk. +% +% Author: Rebecca Cowley +% + +% +% Copyright (c) 2009, eMarine Information Infrastructure (eMII) and Integrated +% Marine Observing System (IMOS). +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright +% notice, this list of conditions and the following disclaimer in the +% documentation and/or other materials provided with the distribution. +% * Neither the name of the eMII/IMOS nor the names of its contributors +% may be used to endorse or promote products derived from this software +% without specific prior written permission. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% +narginchk(4,4); + +if ~iscell(sample_data), error('sample_data must be a cell array'); end +if ~islogical(isQC), error('isQC must be a logical'); end +if ~islogical(saveToFile), error('saveToFile must be a logical'); end +if ~ischar(exportDir), error('exportDir must be a string'); end + +varTitle = imosParameters('DEPTH', 'long_name'); +varUnit = imosParameters('DEPTH', 'uom'); + +stringQC = 'non QC'; +if isQC, stringQC = 'QC'; end + +%plot depth information +monitorRec = get(0,'MonitorPosition'); +xResolution = monitorRec(:, 3)-monitorRec(:, 1); +iBigMonitor = xResolution == max(xResolution); +if sum(iBigMonitor)==2, iBigMonitor(2) = false; end % in case exactly same monitors + +title = [sample_data{1}.deployment_code ' mooring planned depth vs measured depth ' stringQC '''d good ' varTitle]; + +%extract the essential data and +%sort instruments by depth +lenSampleData = length(sample_data); +instrumentDesc = cell(lenSampleData, 1); +metaDepth = nan(lenSampleData, 1); +xMin = nan(lenSampleData, 1); +xMax = nan(lenSampleData, 1); +dataVar = nan(lenSampleData,800000); +timeVar = dataVar; +isPlottable = false; +for i=1:lenSampleData + %only look at instruments with pressure + iPresRel = getVar(sample_data{i}.variables, 'PRES_REL'); + iPres = getVar(sample_data{i}.variables, 'PRES'); + if (iPresRel==0 && iPres==0) + continue; + end + if iPresRel + data = sample_data{i}.variables{iPresRel}.data; + else + data = sample_data{i}.variables{iPres}.data - 14.7*0.689476; % let's apply SeaBird's atmospheric correction + iPresRel = iPres; + end + + iTime = getVar(sample_data{i}.dimensions, 'TIME'); + time = sample_data{i}.dimensions{iTime}.data; + + %calculate depth + iLat = getVar(sample_data{i}.variables, 'LATITUDE'); + if isempty(iLat) + error(['Depth calculation imposible: no latitude documented for ' sample_data{i}.toolbox_input_file ... + ' serial number ' sample_data{i}.instrument_serial_number]); + end + data = -gsw_z_from_p(data, sample_data{i}.variables{iLat}.data); + + iGood = true(size(data)); + + if isQC + %get time and var QC information + timeFlags = sample_data{i}.dimensions{iTime}.flags; + presFlags = sample_data{i}.variables{iPresRel}.flags; + + iGood = (timeFlags == 0 | timeFlags == 1 | timeFlags == 2) ... + & (presFlags == 1 | presFlags == 2); + end + + if all(~iGood) && isQC + fprintf('%s\n', ['Warning : in ' sample_data{i}.toolbox_input_file ... + ', there is not any pressure data with good flags.']); + continue; + else + isPlottable = true; + end + + data = data(iGood); + time = time(iGood); + + %save the data into a holding matrix so we don't have to loop over the + %sample_data matrix again. + dataVar(i,1:length(data)) = data; + timeVar(i,1:length(time)) = time; + + if ~isempty(sample_data{i}.meta.depth) + metaDepth(i) = sample_data{i}.meta.depth; + elseif ~isempty(sample_data{i}.instrument_nominal_depth) + metaDepth(i) = sample_data{i}.instrument_nominal_depth; + else + metaDepth(i) = NaN; + end + + xMin(i) = min(time); + xMax(i) = max(time); + % instrument description + if ~isempty(strtrim(sample_data{(i)}.instrument)) + instrumentDesc{i} = sample_data{(i)}.instrument; + elseif ~isempty(sample_data{(i)}.toolbox_input_file) + [~, instrumentDesc{i}] = fileparts(sample_data{(i)}.toolbox_input_file); + end + + instrumentSN = ''; + if ~isempty(strtrim(sample_data{(i)}.instrument_serial_number)) + instrumentSN = [' - ' sample_data{(i)}.instrument_serial_number]; + end + + instrumentDesc{i} = [strrep(instrumentDesc{i}, '_', ' ') ' (' num2str(metaDepth((i))) 'm' instrumentSN ')']; +end + +if ~isPlottable + return; +end + +%only look at indexes with pres_rel +[metaDepth, iSort] = sort(metaDepth); +dataVar = dataVar(iSort,:); +timeVar = timeVar(iSort,:); +sample_data = sample_data(iSort); +instrumentDesc = instrumentDesc(iSort); +%delete non-pressure instrument information +dataVarTmp = dataVar; +dataVarTmp(isnan(dataVar)) = 0; +ibad = sum(dataVarTmp,2)==0; +metaDepth(ibad) = []; +sample_data(ibad) = []; +dataVar(ibad,:) = []; +timeVar(ibad,:) = []; +instrumentDesc(ibad) = []; +xMin = min(xMin); +xMax = max(xMax); + +%color map +cMap = jet(length(metaDepth)); + +%now plot all the calculated depths on one plot to choose region for comparison: +%plot +fileName = genIMOSFileName(sample_data{1}, 'png'); +visible = 'on'; +if saveToFile, visible = 'off'; end +hFigPress = figure(... + 'Name', title, ... + 'NumberTitle','off', ... + 'Visible', visible, ... + 'OuterPosition', [0, 0, monitorRec(iBigMonitor, 3), monitorRec(iBigMonitor, 4)]); + +%depth plot for selecting region to compare depth to planned depth +hAxPress = subplot(2,1,1,'Parent', hFigPress); +set(hAxPress, 'YDir', 'reverse') +set(get(hAxPress, 'XLabel'), 'String', 'Time'); +set(get(hAxPress, 'YLabel'), 'String', ['DEPTH (' varUnit ')'], 'Interpreter', 'none'); +set(get(hAxPress, 'Title'), 'String', 'Depth', 'Interpreter', 'none'); +set(hAxPress, 'XTick', (xMin:(xMax-xMin)/4:xMax)); +set(hAxPress, 'XLim', [xMin, xMax]); +hold(hAxPress, 'on'); + +%now plot the data:Have to do it one at a time to get the colors right.. +for i = 1:length(metaDepth) + hLineVar(i) = line(timeVar(i,:), ... + dataVar(i,:), ... + 'Color',cMap(i,:),... + 'LineStyle', '-',... + 'Parent',hAxPress); +end + +% set background to be grey +% set(hAxPressDiff, 'Color', [0.75 0.75 0.75]) + +% Let's redefine properties after pcolor to make sure grid lines appear +% above color data and XTick and XTickLabel haven't changed +set(hAxPress, ... + 'XTick', (xMin:(xMax-xMin)/4:xMax), ... + 'XGrid', 'on', ... + 'YGrid', 'on', ... + 'Layer', 'top'); + +if isPlottable + datetick(hAxPress, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); + + hLegend = legend(hAxPress, ... + hLineVar, instrumentDesc, ... + 'Interpreter', 'none', ... + 'Location', 'Best'); + + set(hLegend,'Fontsize',10) +end + +%Ask for the user to select the region they would like to use for +%comparison +%This could be done better, with more finesse - could allow zooming in +%before going straight to the time period selection. For now, this will do. +hMsgbox = msgbox('Select the time period for comparison using the mouse', 'Time Period Selection', 'help', 'modal'); +uiwait(hMsgbox); + +%select the area to use for comparison +[x,y] = select_points(hAxPress); + +%Actual depth minus planned depth +hAxDepthDiff = subplot(2,1,2,'Parent', hFigPress); +set(get(hAxDepthDiff, 'XLabel'), 'String', 'Planned Depth (m)'); +set(get(hAxDepthDiff, 'YLabel'), 'String', ['Actual Depth - Planned Depth (' varUnit ')'], 'Interpreter', 'none'); +set(get(hAxDepthDiff, 'Title'), 'String', ... + ['Differences from planned depth for ' sample_data{1}.meta.site_name] , 'Interpreter', 'none'); +hold(hAxDepthDiff, 'on'); +grid(hAxDepthDiff, 'on'); + + +%now plot the difference from planned depth data: +iGood = timeVar >= x(1) & timeVar <= x(2); +dataVar(~iGood) = NaN; +minDep = min(dataVar,[],2); + +hLineVar2 = line(metaDepth, ... + minDep - metaDepth, ... + 'Color','k',... + 'LineStyle', 'None',... + 'Marker','o',... + 'Marker','.',... + 'MarkerSize',12,... + 'Parent',hAxDepthDiff); + +text(metaDepth + 1, (minDep - metaDepth), instrumentDesc, ... + 'Parent', hAxDepthDiff) + +if isPlottable + if saveToFile + % ensure the printed version is the same whatever the screen used. + set(hFigPress, 'PaperPositionMode', 'manual'); + set(hFigPress, 'PaperType', 'A4', 'PaperOrientation', 'landscape', 'PaperUnits', 'normalized', 'PaperPosition', [0, 0, 1, 1]); + + % preserve the color scheme + set(hFigPress, 'InvertHardcopy', 'off'); + + fileName = strrep(fileName, '_PARAM_', ['_', varName, '_']); % IMOS_[sub-facility_code]_[site_code]_FV01_[deployment_code]_[PLOT-TYPE]_[PARAM]_C-[creation_date].png + fileName = strrep(fileName, '_PLOT-TYPE_', '_LINE_'); + + % use hardcopy as a trick to go faster than print. + % opengl (hardware or software) should be supported by any platform and go at least just as + % fast as zbuffer. With hardware accelaration supported, could even go a + % lot faster. + imwrite(hardcopy(hFigPress, '-dopengl'), fullfile(exportDir, fileName), 'png'); + close(hFigPress); + end +end + +end \ No newline at end of file diff --git a/Graph/checkMooringPresDiffs.m b/Graph/checkMooringPresDiffs.m new file mode 100644 index 000000000..970985b02 --- /dev/null +++ b/Graph/checkMooringPresDiffs.m @@ -0,0 +1,329 @@ +function checkMooringPresDiffs(sample_data, iSampleMenu, isQC, saveToFile, exportDir) +%CHECKMOORINGPRESDIFFS Opens a new window where the pressure +% variable collected by the selected intrument is plotted, and the difference +% in pressure from this instrument to adjacent instruments is plotted. +% +% Inputs: +% sample_data - cell array of structs containing the entire data set and dimension data. +% +% iSampleMenu - current Value of sampleMenu. +% +% isQC - logical to plot only good data or not. +% +% saveToFile - logical to save the plot on disk or not. +% +% exportDir - string containing the destination folder to where the +% plot is saved on disk. +% +% Author: Rebecca Cowley +% + +% +% Copyright (c) 2009, eMarine Information Infrastructure (eMII) and Integrated +% Marine Observing System (IMOS). +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are met: +% +% * Redistributions of source code must retain the above copyright notice, +% this list of conditions and the following disclaimer. +% * Redistributions in binary form must reproduce the above copyright +% notice, this list of conditions and the following disclaimer in the +% documentation and/or other materials provided with the distribution. +% * Neither the name of the eMII/IMOS nor the names of its contributors +% may be used to endorse or promote products derived from this software +% without specific prior written permission. +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. +% +narginchk(5,5); + +if ~iscell(sample_data), error('sample_data must be a cell array'); end +if ~islogical(isQC), error('isQC must be a logical'); end +if ~islogical(saveToFile), error('saveToFile must be a logical'); end +if ~ischar(exportDir), error('exportDir must be a string'); end + +presRelCode = 'PRES_REL'; +presCode = 'PRES'; +varTitle = imosParameters(presRelCode, 'long_name'); +varUnit = imosParameters(presRelCode, 'uom'); + +stringQC = 'non QC'; +if isQC, stringQC = 'QC'; end + +%plot depth information +monitorRec = get(0,'MonitorPosition'); +xResolution = monitorRec(:, 3)-monitorRec(:, 1); +iBigMonitor = xResolution == max(xResolution); +if sum(iBigMonitor)==2, iBigMonitor(2) = false; end % in case exactly same monitors + +title = [sample_data{1}.deployment_code ' mooring pressure differences ' stringQC '''d ' varTitle]; + +%sort instruments by depth +lenSampleData = length(sample_data); +instrumentDesc = cell(lenSampleData, 1); +metaDepth = nan(lenSampleData, 1); +xMin = nan(lenSampleData, 1); +xMax = nan(lenSampleData, 1); +iPresRel = nan(lenSampleData, 1); +iPres = nan(lenSampleData, 1); +for i=1:lenSampleData + if ~isempty(sample_data{i}.meta.depth) + metaDepth(i) = sample_data{i}.meta.depth; + elseif ~isempty(sample_data{i}.instrument_nominal_depth) + metaDepth(i) = sample_data{i}.instrument_nominal_depth; + else + metaDepth(i) = NaN; + end + + % instrument description + if ~isempty(strtrim(sample_data{i}.instrument)) + instrumentDesc{i} = sample_data{i}.instrument; + elseif ~isempty(sample_data{i}.toolbox_input_file) + [~, instrumentDesc{i}] = fileparts(sample_data{i}.toolbox_input_file); + end + + instrumentSN = ''; + if ~isempty(strtrim(sample_data{i}.instrument_serial_number)) + instrumentSN = [' - ' sample_data{i}.instrument_serial_number]; + end + + instrumentDesc{i} = [strrep(instrumentDesc{i}, '_', ' ') ' (' num2str(metaDepth(i)) 'm' instrumentSN ')']; + + iTime = getVar(sample_data{i}.dimensions, 'TIME'); + %check for pressure + iPresRel(i) = getVar(sample_data{i}.variables, presRelCode); + iPres(i) = getVar(sample_data{i}.variables, presCode); + + xMin(i) = min(sample_data{i}.dimensions{iTime}.data); + xMax(i) = max(sample_data{i}.dimensions{iTime}.data); +end +%only look at indexes with pres_rel or pres +[metaDepth, iSort] = sort(metaDepth); +sample_data = sample_data(iSort); +instrumentDesc = instrumentDesc(iSort); +iPresRel = iPresRel(iSort); +iPres = iPres(iSort); + +iSort(iPresRel==0 & iPres==0) = []; +metaDepth(iPresRel==0 & iPres==0) = []; +instrumentDesc(iPresRel==0 & iPres==0) = []; +sample_data(iPresRel==0 & iPres==0) = []; + +xMin = min(xMin); +xMax = max(xMax); + +%first find the instrument of interest: +iCurrSam = find(iSort == iSampleMenu); + +hLineVar = nan(length(metaDepth), 1); +hLineVar2 = hLineVar; + +isPlottable = false; + +%plot +fileName = genIMOSFileName(sample_data{iCurrSam}, 'png'); +visible = 'on'; +if saveToFile, visible = 'off'; end +hFigPressDiff = figure(... + 'Name', title, ... + 'NumberTitle','off', ... + 'Visible', visible, ... + 'OuterPosition', [0, 0, monitorRec(iBigMonitor, 3), monitorRec(iBigMonitor, 4)]); + +%pressure plot +hAxPress = subplot(2,1,1,'Parent', hFigPressDiff); +set(hAxPress, 'YDir', 'reverse') +set(get(hAxPress, 'XLabel'), 'String', 'Time'); +set(get(hAxPress, 'YLabel'), 'String', [presRelCode ' (' varUnit ')'], 'Interpreter', 'none'); +set(get(hAxPress, 'Title'), 'String', varTitle, 'Interpreter', 'none'); +set(hAxPress, 'XTick', (xMin:(xMax-xMin)/4:xMax)); +set(hAxPress, 'XLim', [xMin, xMax]); +hold(hAxPress, 'on'); + +%Pressure diff plot +hAxPressDiff = subplot(2,1,2,'Parent', hFigPressDiff); +set(get(hAxPressDiff, 'XLabel'), 'String', 'Time'); +set(get(hAxPressDiff, 'YLabel'), 'String', [presRelCode ' (' varUnit ')'], 'Interpreter', 'none'); +set(get(hAxPressDiff, 'Title'), 'String', ... + ['Pressure Differences from ' instrumentDesc{iCurrSam} ' (in black above)'] , 'Interpreter', 'none'); +set(hAxPressDiff, 'XTick', (xMin:(xMax-xMin)/4:xMax)); +set(hAxPressDiff, 'XLim', [xMin, xMax]); +hold(hAxPressDiff, 'on'); + +linkaxes([hAxPressDiff,hAxPress],'x') + +%zero line +line([xMin, xMax], [0, 0], 'Color', 'black'); + +%now plot the data of interest: +iCurrTime = getVar(sample_data{iCurrSam}.dimensions, 'TIME'); +curSamTime = sample_data{iCurrSam}.dimensions{iCurrTime}.data; + +iCurrPresRel = getVar(sample_data{iCurrSam}.variables, presRelCode); +iCurrPres = getVar(sample_data{iCurrSam}.variables, presCode); +if iCurrPresRel + curSamPresRel = sample_data{iCurrSam}.variables{iCurrPresRel}.data; +else + curSamPresRel = sample_data{iCurrSam}.variables{iCurrPres}.data - 14.7*0.689476; % let's apply SeaBird's atmospheric correction + iCurrPresRel = iCurrPres; +end + +iGood = true(size(curSamPresRel)); +if isQC + %get time and var QC information + timeFlags = sample_data{iCurrSam}.dimensions{iCurrTime}.flags; + varFlags = sample_data{iCurrSam}.variables{iCurrPresRel}.flags; + + iGood = (timeFlags == 0 | timeFlags == 1 | timeFlags == 2) & (varFlags == 1 | varFlags == 2); +end + +curSamTime(~iGood) = NaN; +curSamPresRel(~iGood) = NaN; + +hLineVar(iCurrSam) = line(curSamTime, ... + curSamPresRel, ... + 'Color', 'k', ... + 'LineStyle', '-',... + 'Parent',hAxPress); + +%now get the adjacent instruments based on planned depth (up to 4 nearest) +metaDepthCurrSam = metaDepth(iCurrSam); +% sample_data(iCurrSam) = []; +% metaDepth(iCurrSam) = []; +[~, iOthers] = sort(abs(metaDepthCurrSam - metaDepth)); +iOthers(1) = []; % remove current sample +nOthers = length(iOthers); +if nOthers > 4 + iOthers = iOthers(1:4); + nOthers = 4; +end + +%color map +cMap = hsv(nOthers); + +%now add the other data: +for i=1:nOthers + %look for time and relevant variable + iOtherTime = getVar(sample_data{iOthers(i)}.dimensions, 'TIME'); + otherSamTime = sample_data{iOthers(i)}.dimensions{iOtherTime}.data; + + iOtherPresRel = getVar(sample_data{iOthers(i)}.variables, presRelCode); + iOtherPres = getVar(sample_data{iOthers(i)}.variables, presCode); + if iOtherPresRel + otherSamPresRel = sample_data{iOthers(i)}.variables{iOtherPresRel}.data; + else + otherSamPresRel = sample_data{iOthers(i)}.variables{iOtherPres}.data - 14.7*0.689476; % let's apply SeaBird's atmospheric correction + iOtherPresRel = iOtherPres; + end + + iGood = true(size(otherSamPresRel)); + if isQC + %get time and var QC information + timeFlags = sample_data{iOthers(i)}.dimensions{iOtherTime}.flags; + varFlags = sample_data{iOthers(i)}.variables{iOtherPresRel}.flags; + + iGood = (timeFlags == 0 | timeFlags == 1 | timeFlags == 2) & (varFlags == 1 | varFlags == 2); + end + + if all(~iGood) && isQC + fprintf('%s\n', ['Warning : in ' sample_data{iOthers(i)}.toolbox_input_file ... + ', there is not any pressure data with good flags.']); + continue; + else + isPlottable = true; + + otherSamTime(~iGood) = []; + otherSamPresRel(~iGood) = []; + + %add pressure to the pressure plot + hLineVar(iOthers(i)) = line(otherSamTime, ... + otherSamPresRel, ... + 'Color', cMap(i, :), ... + 'LineStyle', '-','Parent',hAxPress); + + %now put the data on the same timebase as the instrument of + %interest + newdat = interp1(otherSamTime,otherSamPresRel,curSamTime); + pdiff = curSamPresRel - newdat; + + hLineVar2(iOthers(i)) = line(curSamTime, ... + pdiff, ... + 'Color', cMap(i, :), ... + 'LineStyle', '-','Parent',hAxPressDiff); + + % set background to be grey +% set(hAxPressDiff, 'Color', [0.75 0.75 0.75]) + end +end + +% Let's redefine properties after pcolor to make sure grid lines appear +% above color data and XTick and XTickLabel haven't changed +set(hAxPress, ... + 'XTick', (xMin:(xMax-xMin)/4:xMax), ... + 'XGrid', 'on', ... + 'YGrid', 'on', ... + 'Layer', 'top'); + +set(hAxPressDiff, ... + 'XTick', (xMin:(xMax-xMin)/4:xMax), ... + 'XGrid', 'on', ... + 'YGrid', 'on', ... + 'Layer', 'top'); + +if isPlottable + iNan = isnan(hLineVar); + if any(iNan) + hLineVar(iNan) = []; + instrumentDesc(iNan) = []; + end + + datetick(hAxPressDiff, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); + datetick(hAxPress, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); + + hLegend = legend(hAxPress, ... + hLineVar, instrumentDesc, ... + 'Interpreter', 'none', ... + 'Location', 'SouthOutside'); + + set(hLegend,'Fontsize',14) + +% % unfortunately we need to do this hack so that we have consistency with +% % the case above +% posAx = get(hAxPressDiff, 'Position'); +% set(hAxPressDiff, 'Position', posAx); + + % set(hLegend, 'Box', 'off', 'Color', 'none'); + + if saveToFile + % ensure the printed version is the same whatever the screen used. + set(hFigPressDiff, 'PaperPositionMode', 'manual'); + set(hFigPressDiff, 'PaperType', 'A4', 'PaperOrientation', 'landscape', 'PaperUnits', 'normalized', 'PaperPosition', [0, 0, 1, 1]); + + % preserve the color scheme + set(hFigPressDiff, 'InvertHardcopy', 'off'); + + fileName = strrep(fileName, '_PARAM_', ['_', varName, '_']); % IMOS_[sub-facility_code]_[site_code]_FV01_[deployment_code]_[PLOT-TYPE]_[PARAM]_C-[creation_date].png + fileName = strrep(fileName, '_PLOT-TYPE_', '_LINE_'); + + % use hardcopy as a trick to go faster than print. + % opengl (hardware or software) should be supported by any platform and go at least just as + % fast as zbuffer. With hardware accelaration supported, could even go a + % lot faster. + imwrite(hardcopy(hFigPressDiff, '-dopengl'), fullfile(exportDir, fileName), 'png'); + close(hFigPressDiff); + end +end + +end \ No newline at end of file diff --git a/Util/select_points.m b/Util/select_points.m new file mode 100644 index 000000000..baa61c2cc --- /dev/null +++ b/Util/select_points.m @@ -0,0 +1,20 @@ +function [x,y] = select_points(hAx) +%function [x,y] = select_points +% Uses rbbox to select points in the timeseries chart for flagging. +% Returns [x,y] - index of rectangle corners in figure units +axes(hAx); +k = waitforbuttonpress; +point1 = get(gca,'CurrentPoint'); % button down detected +finalRect = rbbox; % return figure units +point2 = get(gca,'CurrentPoint'); % button up detected +point1 = point1(1,1:2); % extract x and y +point2 = point2(1,1:2); +p1 = min(point1,point2); % calculate locations +offset = abs(point1-point2); % and dimensions +x = [p1(1) p1(1)+offset(1) p1(1)+offset(1) p1(1) p1(1)]; +y = [p1(2) p1(2) p1(2)+offset(2) p1(2)+offset(2) p1(2)]; +hold on +axis manual +plot(x,y); % redraw in dataspace units + +end \ No newline at end of file From 3ad27f9318d14bc0848d281aba543eb8793fad7e Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Mon, 11 Apr 2016 16:03:55 +1000 Subject: [PATCH 16/21] Only display Y label for the first plot when in profile mode. --- Graph/graphDepthProfile.m | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Graph/graphDepthProfile.m b/Graph/graphDepthProfile.m index 604451c16..693f0495d 100644 --- a/Graph/graphDepthProfile.m +++ b/Graph/graphDepthProfile.m @@ -161,13 +161,15 @@ xLabel = [labels{1} uom]; set(get(graphs(k), 'XLabel'), 'String', xLabel, 'Interpreter', 'none'); - % set y label - try uom = [' (' imosParameters(labels{2}, 'uom') ')']; - catch e, uom = ''; + % set y label for the first plot + if k==1 + try uom = [' (' imosParameters(labels{2}, 'uom') ')']; + catch e, uom = ''; + end + yLabel = [labels{2} uom]; + if length(yLabel) > 20, yLabel = [yLabel(1:17) '...']; end + set(get(graphs(k), 'YLabel'), 'String', yLabel, 'Interpreter', 'none'); end - yLabel = [labels{2} uom]; - if length(yLabel) > 20, yLabel = [yLabel(1:17) '...']; end - set(get(graphs(k), 'YLabel'), 'String', yLabel, 'Interpreter', 'none'); if sample_data.meta.level == 1 && strcmp(func2str(plotFunc), 'graphDepthProfileGeneric') qcSet = str2double(readProperty('toolbox.qc_set')); From beab42005e68eea6068a8402238a2a1d3d41494f Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Wed, 13 Apr 2016 16:09:51 +1000 Subject: [PATCH 17/21] Fix description and sorting of data_samples for profile mode. --- GUI/dataFileStatusDialog.m | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/GUI/dataFileStatusDialog.m b/GUI/dataFileStatusDialog.m index d18ba1086..9cddce0a6 100644 --- a/GUI/dataFileStatusDialog.m +++ b/GUI/dataFileStatusDialog.m @@ -64,16 +64,30 @@ origDeployments = deployments; origFiles = files; + % get the toolbox execution mode. Values can be 'timeSeries' and 'profile'. + % If no value is set then default mode is 'timeSeries' + mode = lower(readProperty('toolbox.mode')); + deploymentDescs = genDepDescriptions(deployments, files); - % sort deployments by depth + % Sort data_samples % % [B, iX] = sort(A); % => % A(iX) == B % - [~, iSort] = sort([deployments.InstrumentDepth]); - deploymentDescs = deploymentDescs(iSort); + switch mode + case 'profile' + % for a profile, sort by alphabetical order + [deploymentDescs, iSort] = sort(deploymentDescs); + otherwise + % for a mooring, sort instruments by depth + [~, iSort] = sort([deployments.InstrumentDepth]); + deploymentDescs = deploymentDescs(iSort); + end + + + deployments = deployments(iSort); files = files(iSort); @@ -263,11 +277,6 @@ function fileAddCallback(source,ev) % Opens a dialog, allowing the user to select a file to add to the % deployment. % - - % get the toolbox execution mode. Values can be 'timeSeries' and 'profile'. - % If no value is set then default mode is 'timeSeries' - mode = lower(readProperty('toolbox.mode')); - switch mode case 'profile' [newFile path] = uigetfile('*', 'Select Data File',... @@ -318,8 +327,10 @@ function fileAddCallback(source,ev) if ~isempty(dep.InstrumentDepth) descs{k} = [descs{k} ' @' num2str(dep.InstrumentDepth) 'm']; end - if ~isempty(dep.DepthTxt) - descs{k} = [descs{k} ' ' dep.DepthTxt]; + if isfield(dep, 'DepthTxt') + if ~isempty(dep.DepthTxt) + descs{k} = [descs{k} ' ' dep.DepthTxt]; + end end if ~isempty(dep.FileName) descs{k} = [descs{k} ' (' dep.FileName ')']; From 75f748bd38d01220fe0697511574b85c7d8c1851 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Wed, 13 Apr 2016 16:25:40 +1000 Subject: [PATCH 18/21] -Default color palette parula instead of jet. -Data cursor enabled on QC plots with multiple instruments per parameter. -Improved legend display on QC plots. -fastScatterMesh replaces scatter on QC plots. --- .../setTimeSerieColorbarContextMenu.m | 42 +- Graph/checkMooringPlannedDepths.m | 104 ++- Graph/checkMooringPresDiffs.m | 72 +- Graph/lineCastVar.m | 2 +- Graph/lineMooring1DVar.m | 212 +++-- Graph/lineMooring2DVarSection.m | 13 +- Graph/pcolorMooring2DVar.m | 4 +- Graph/scatterMooring1DVarAgainstDepth.m | 324 +++++-- Graph/scatterMooring2DVarAgainstDepth.m | 333 +++++-- Util/fastScatterMesh.m | 97 ++ Util/getpos.m | 180 ++++ Util/legendflex.m | 827 ++++++++++++++++++ Util/parula.m | 77 ++ Util/plotclr.m | 8 +- Util/setpos.m | 187 ++++ Util/viridis.m | 269 ++++++ toolboxProperties.txt | 9 + 17 files changed, 2456 insertions(+), 304 deletions(-) create mode 100644 Util/fastScatterMesh.m create mode 100644 Util/getpos.m create mode 100644 Util/legendflex.m create mode 100644 Util/parula.m create mode 100644 Util/setpos.m create mode 100644 Util/viridis.m diff --git a/Graph/TimeSeries/setTimeSerieColorbarContextMenu.m b/Graph/TimeSeries/setTimeSerieColorbarContextMenu.m index 6391e6611..8892cbcbc 100644 --- a/Graph/TimeSeries/setTimeSerieColorbarContextMenu.m +++ b/Graph/TimeSeries/setTimeSerieColorbarContextMenu.m @@ -105,40 +105,44 @@ uimenu(mainItem2, 'Label', 'manual', 'Callback', {@cbCLimRange, 'manual', var.data}); case 'PERG' % percentages - colormap(jet); + colormap(parula); cbCLimRange('', '', 'percent [0; 100]', var.data); % Define a context menu hMenu = uicontextmenu; % Define callbacks for context menu items that change linestyle - hcb11 = 'colormap(jet)'; + hcb11 = 'colormap(parula)'; + hcb12 = 'colormap(jet)'; hcb13 = 'colormapeditor'; % Define the context menu items and install their callbacks mainItem1 = uimenu(hMenu, 'Label', 'Colormaps'); - uimenu(mainItem1, 'Label', 'jet (default)', 'Callback', hcb11); - uimenu(mainItem1, 'Label', 'other', 'Callback', hcb13); + uimenu(mainItem1, 'Label', 'parula (default)', 'Callback', hcb11); + uimenu(mainItem1, 'Label', 'jet', 'Callback', hcb12); + uimenu(mainItem1, 'Label', 'other', 'Callback', hcb13); mainItem2 = uimenu(hMenu, 'Label', 'Color range'); uimenu(mainItem2, 'Label', 'percent [0; 100] (default)', 'Callback', {@cbCLimRange, 'percent [0; 100]', var.data}); uimenu(mainItem2, 'Label', 'manual', 'Callback', {@cbCLimRange, 'manual', var.data}); case {'CSPD', 'VDEN', 'VDEV', 'VDEP', 'VDES'} % [0; oo[ paremeters - colormap(jet); + colormap(parula); cbCLimRange('', '', 'auto from 0', var.data); % Define a context menu hMenu = uicontextmenu; % Define callbacks for context menu items that change linestyle + hcb11 = 'colormap(parula)'; hcb12 = 'colormap(jet)'; hcb13 = 'colormapeditor'; % Define the context menu items and install their callbacks mainItem1 = uimenu(hMenu, 'Label', 'Colormaps'); - uimenu(mainItem1, 'Label', 'jet (default)', 'Callback', hcb12); - uimenu(mainItem1, 'Label', 'other', 'Callback', hcb13); + uimenu(mainItem1, 'Label', 'parula (default)', 'Callback', hcb11); + uimenu(mainItem1, 'Label', 'jet', 'Callback', hcb12); + uimenu(mainItem1, 'Label', 'other', 'Callback', hcb13); mainItem2 = uimenu(hMenu, 'Label', 'Color range'); uimenu(mainItem2, 'Label', 'full', 'Callback', {@cbCLimRange, 'full', var.data}); @@ -157,15 +161,17 @@ hMenu = uicontextmenu; % Define callbacks for context menu items that change linestyle - hcb12 = 'load(''jet_w.mat'', ''-mat'', ''jet_w''); colormap(jet_w)'; + hcb11 = 'load(''jet_w.mat'', ''-mat'', ''jet_w''); colormap(jet_w)'; + hcb12 = 'colormap(parula)'; hcb13 = 'colormap(jet)'; hcb14 = 'colormapeditor'; % Define the context menu items and install their callbacks mainItem1 = uimenu(hMenu, 'Label', 'Colormaps'); - uimenu(mainItem1, 'Label', 'jet_w (default)', 'Callback', hcb12); - uimenu(mainItem1, 'Label', 'jet', 'Callback', hcb13); - uimenu(mainItem1, 'Label', 'other', 'Callback', hcb14); + uimenu(mainItem1, 'Label', 'jet_w (default)', 'Callback', hcb11); + uimenu(mainItem1, 'Label', 'parula', 'Callback', hcb12); + uimenu(mainItem1, 'Label', 'jet', 'Callback', hcb13); + uimenu(mainItem1, 'Label', 'other', 'Callback', hcb14); mainItem2 = uimenu(hMenu, 'Label', 'Color range'); uimenu(mainItem2, 'Label', 'full', 'Callback', {@cbCLimRange, 'full', var.data}); @@ -175,22 +181,24 @@ uimenu(mainItem2, 'Label', 'manual', 'Callback', {@cbCLimRange, 'manual', var.data}); otherwise - colormap(jet); + colormap(parula); cbCLimRange('', '', 'full', var.data); % Define a context menu hMenu = uicontextmenu; % Define callbacks for context menu items that change linestyle - hcb11 = 'colormap(r_b)'; + hcb11 = 'colormap(parula)'; hcb12 = 'colormap(jet)'; - hcb13 = 'colormapeditor'; + hcb13 = 'colormap(r_b)'; + hcb14 = 'colormapeditor'; % Define the context menu items and install their callbacks mainItem1 = uimenu(hMenu, 'Label', 'Colormaps'); - uimenu(mainItem1, 'Label', 'r_b', 'Callback', hcb11); - uimenu(mainItem1, 'Label', 'jet (default)', 'Callback', hcb12); - uimenu(mainItem1, 'Label', 'other', 'Callback', hcb13); + uimenu(mainItem1, 'Label', 'parula (default)', 'Callback', hcb11); + uimenu(mainItem1, 'Label', 'jet', 'Callback', hcb12); + uimenu(mainItem1, 'Label', 'r_b', 'Callback', hcb13); + uimenu(mainItem1, 'Label', 'other', 'Callback', hcb14); mainItem2 = uimenu(hMenu, 'Label', 'Color range'); uimenu(mainItem2, 'Label', 'full (default)', 'Callback', {@cbCLimRange, 'full', var.data}); diff --git a/Graph/checkMooringPlannedDepths.m b/Graph/checkMooringPlannedDepths.m index 88c0807b1..8dcaa9d48 100644 --- a/Graph/checkMooringPlannedDepths.m +++ b/Graph/checkMooringPlannedDepths.m @@ -72,12 +72,16 @@ function checkMooringPlannedDepths(sample_data, isQC, saveToFile, exportDir) %sort instruments by depth lenSampleData = length(sample_data); instrumentDesc = cell(lenSampleData, 1); +hLineVar = nan(lenSampleData, 1); metaDepth = nan(lenSampleData, 1); xMin = nan(lenSampleData, 1); xMax = nan(lenSampleData, 1); dataVar = nan(lenSampleData,800000); timeVar = dataVar; isPlottable = false; + +backgroundColor = [0.75 0.75 0.75]; + for i=1:lenSampleData %only look at instruments with pressure iPresRel = getVar(sample_data{i}.variables, 'PRES_REL'); @@ -177,8 +181,8 @@ function checkMooringPlannedDepths(sample_data, isQC, saveToFile, exportDir) xMin = min(xMin); xMax = max(xMax); -%color map -cMap = jet(length(metaDepth)); +instrumentDesc = [{'Make Model (nominal depth - instrument SN)'}; instrumentDesc]; +hLineVar(1) = 0; %now plot all the calculated depths on one plot to choose region for comparison: %plot @@ -191,8 +195,10 @@ function checkMooringPlannedDepths(sample_data, isQC, saveToFile, exportDir) 'Visible', visible, ... 'OuterPosition', [0, 0, monitorRec(iBigMonitor, 3), monitorRec(iBigMonitor, 4)]); -%depth plot for selecting region to compare depth to planned depth hAxPress = subplot(2,1,1,'Parent', hFigPress); +hAxDepthDiff = subplot(2,1,2,'Parent', hFigPress); + +%depth plot for selecting region to compare depth to planned depth set(hAxPress, 'YDir', 'reverse') set(get(hAxPress, 'XLabel'), 'String', 'Time'); set(get(hAxPress, 'YLabel'), 'String', ['DEPTH (' varUnit ')'], 'Interpreter', 'none'); @@ -201,18 +207,41 @@ function checkMooringPlannedDepths(sample_data, isQC, saveToFile, exportDir) set(hAxPress, 'XLim', [xMin, xMax]); hold(hAxPress, 'on'); +%Actual depth minus planned depth +set(get(hAxDepthDiff, 'XLabel'), 'String', 'Planned Depth (m)'); +set(get(hAxDepthDiff, 'YLabel'), 'String', ['Actual Depth - Planned Depth (' varUnit ')'], 'Interpreter', 'none'); +set(get(hAxDepthDiff, 'Title'), 'String', ... + ['Differences from planned depth for ' sample_data{1}.meta.site_name] , 'Interpreter', 'none'); +hold(hAxDepthDiff, 'on'); +grid(hAxDepthDiff, 'on'); + +% set background to be grey +set(hAxPress, 'Color', backgroundColor); +set(hAxDepthDiff, 'Color', backgroundColor); + +%color map +lenMetaDepth = length(metaDepth); +try + defaultColormapFh = str2func(readProperty('visualQC.defaultColormap')); + cMap = colormap(hAxPress, defaultColormapFh(lenMetaDepth)); +catch e + cMap = colormap(hAxPress, parula(lenMetaDepth)); +end +% reverse the colorbar as we want surface instruments with warmer colors +cMap = flipud(cMap); + +% dummy entry for first entry in legend +hLineVar(1) = plot(hAxPress, 0, 0, 'Color', backgroundColor, 'Visible', 'off'); % color grey same as background (invisible) + %now plot the data:Have to do it one at a time to get the colors right.. -for i = 1:length(metaDepth) - hLineVar(i) = line(timeVar(i,:), ... +for i = 1:lenMetaDepth + hLineVar(i+1) = line(timeVar(i,:), ... dataVar(i,:), ... - 'Color',cMap(i,:),... + 'Color', cMap(i,:),... 'LineStyle', '-',... - 'Parent',hAxPress); + 'Parent', hAxPress); end -% set background to be grey -% set(hAxPressDiff, 'Color', [0.75 0.75 0.75]) - % Let's redefine properties after pcolor to make sure grid lines appear % above color data and XTick and XTickLabel haven't changed set(hAxPress, ... @@ -224,12 +253,32 @@ function checkMooringPlannedDepths(sample_data, isQC, saveToFile, exportDir) if isPlottable datetick(hAxPress, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); - hLegend = legend(hAxPress, ... - hLineVar, instrumentDesc, ... - 'Interpreter', 'none', ... - 'Location', 'Best'); - - set(hLegend,'Fontsize',10) + % we try to split the legend, maximum 3 columns + fontSizeAx = get(hAxPress,'FontSize'); + fontSizeLb = get(get(hAxPress,'XLabel'),'FontSize'); + xscale = 0.9; + if numel(instrumentDesc) < 4 + nCols = 1; + elseif numel(instrumentDesc) < 8 + nCols = 2; + else + nCols = 3; + fontSizeAx = fontSizeAx - 1; + xscale = 0.75; + end + hYBuffer = 1.1 * (2*(fontSizeAx + fontSizeLb)); + hLegend = legendflex(hAxPress, instrumentDesc,... + 'anchor', [6 2], ... + 'buffer', [0 -hYBuffer], ... + 'ncol', nCols,... + 'FontSize', fontSizeAx,... + 'xscale',xscale); + posAx = get(hAxPress, 'Position'); + set(hLegend, 'Units', 'Normalized', 'color', backgroundColor); + + % for some reason this call brings everything back together while it + % shouldn't have moved previously anyway... + set(hAxPress, 'Position', posAx); end %Ask for the user to select the region they would like to use for @@ -242,30 +291,19 @@ function checkMooringPlannedDepths(sample_data, isQC, saveToFile, exportDir) %select the area to use for comparison [x,y] = select_points(hAxPress); -%Actual depth minus planned depth -hAxDepthDiff = subplot(2,1,2,'Parent', hFigPress); -set(get(hAxDepthDiff, 'XLabel'), 'String', 'Planned Depth (m)'); -set(get(hAxDepthDiff, 'YLabel'), 'String', ['Actual Depth - Planned Depth (' varUnit ')'], 'Interpreter', 'none'); -set(get(hAxDepthDiff, 'Title'), 'String', ... - ['Differences from planned depth for ' sample_data{1}.meta.site_name] , 'Interpreter', 'none'); -hold(hAxDepthDiff, 'on'); -grid(hAxDepthDiff, 'on'); - - %now plot the difference from planned depth data: iGood = timeVar >= x(1) & timeVar <= x(2); dataVar(~iGood) = NaN; minDep = min(dataVar,[],2); -hLineVar2 = line(metaDepth, ... +hLineVar2 = scatter(hAxDepthDiff, ... + metaDepth, ... minDep - metaDepth, ... - 'Color','k',... - 'LineStyle', 'None',... - 'Marker','o',... - 'Marker','.',... - 'MarkerSize',12,... - 'Parent',hAxDepthDiff); + 15, ... + cMap, ... + 'filled'); +instrumentDesc(1) = []; text(metaDepth + 1, (minDep - metaDepth), instrumentDesc, ... 'Parent', hAxDepthDiff) diff --git a/Graph/checkMooringPresDiffs.m b/Graph/checkMooringPresDiffs.m index 970985b02..a12bbef7f 100644 --- a/Graph/checkMooringPresDiffs.m +++ b/Graph/checkMooringPresDiffs.m @@ -132,6 +132,8 @@ function checkMooringPresDiffs(sample_data, iSampleMenu, isQC, saveToFile, expor isPlottable = false; +backgroundColor = [0.75 0.75 0.75]; + %plot fileName = genIMOSFileName(sample_data{iCurrSam}, 'png'); visible = 'on'; @@ -192,26 +194,27 @@ function checkMooringPresDiffs(sample_data, iSampleMenu, isQC, saveToFile, expor curSamTime(~iGood) = NaN; curSamPresRel(~iGood) = NaN; -hLineVar(iCurrSam) = line(curSamTime, ... - curSamPresRel, ... - 'Color', 'k', ... - 'LineStyle', '-',... - 'Parent',hAxPress); - %now get the adjacent instruments based on planned depth (up to 4 nearest) metaDepthCurrSam = metaDepth(iCurrSam); -% sample_data(iCurrSam) = []; -% metaDepth(iCurrSam) = []; [~, iOthers] = sort(abs(metaDepthCurrSam - metaDepth)); -iOthers(1) = []; % remove current sample nOthers = length(iOthers); -if nOthers > 4 - iOthers = iOthers(1:4); - nOthers = 4; +nOthersMax = 5; % includes current sample +if nOthers > nOthersMax + iOthers = iOthers(1:nOthersMax); + nOthers = nOthersMax; end %color map -cMap = hsv(nOthers); +% no need to reverse the colorbar since instruments are plotted from +% nearest (blue) to farthest (yellow) +try + defaultColormapFh = str2func(readProperty('visualQC.defaultColormap')); + cMap = colormap(hAxPress, defaultColormapFh(nOthers - 1)); +catch e + cMap = colormap(hAxPress, parula(nOthers - 1)); +end +% current sample is black +cMap = [0, 0, 0; cMap]; %now add the other data: for i=1:nOthers @@ -264,7 +267,8 @@ function checkMooringPresDiffs(sample_data, iSampleMenu, isQC, saveToFile, expor 'LineStyle', '-','Parent',hAxPressDiff); % set background to be grey -% set(hAxPressDiff, 'Color', [0.75 0.75 0.75]) + set(hAxPress, 'Color', backgroundColor) + set(hAxPressDiff, 'Color', backgroundColor) end end @@ -288,23 +292,37 @@ function checkMooringPresDiffs(sample_data, iSampleMenu, isQC, saveToFile, expor hLineVar(iNan) = []; instrumentDesc(iNan) = []; end + iOthers = iOthers - (min(iOthers)-1); datetick(hAxPressDiff, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); datetick(hAxPress, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); - hLegend = legend(hAxPress, ... - hLineVar, instrumentDesc, ... - 'Interpreter', 'none', ... - 'Location', 'SouthOutside'); - - set(hLegend,'Fontsize',14) - -% % unfortunately we need to do this hack so that we have consistency with -% % the case above -% posAx = get(hAxPressDiff, 'Position'); -% set(hAxPressDiff, 'Position', posAx); - - % set(hLegend, 'Box', 'off', 'Color', 'none'); + % we try to split the legend, maximum 3 columns + fontSizeAx = get(hAxPress,'FontSize'); + fontSizeLb = get(get(hAxPress,'XLabel'),'FontSize'); + xscale = 0.9; + if numel(instrumentDesc) < 4 + nCols = 1; + elseif numel(instrumentDesc) < 8 + nCols = 2; + else + nCols = 3; + fontSizeAx = fontSizeAx - 1; + xscale = 0.75; + end + hYBuffer = 1.1 * (2*(fontSizeAx + fontSizeLb)); + hLegend = legendflex(hAxPress, instrumentDesc(iOthers),... + 'anchor', [6 2], ... + 'buffer', [0 -hYBuffer], ... + 'ncol', nCols,... + 'FontSize', fontSizeAx,... + 'xscale',xscale); + posAx = get(hAxPress, 'Position'); + set(hLegend, 'Units', 'Normalized', 'color', backgroundColor); + + % for some reason this call brings everything back together while it + % shouldn't have moved previously anyway... + set(hAxPress, 'Position', posAx); if saveToFile % ensure the printed version is the same whatever the screen used. diff --git a/Graph/lineCastVar.m b/Graph/lineCastVar.m index 7835f00c5..f209e25ac 100644 --- a/Graph/lineCastVar.m +++ b/Graph/lineCastVar.m @@ -162,7 +162,7 @@ function lineCastVar(sample_data, varNames, isQC, saveToFile, exportDir) set(hAxCastVar, 'YLim', [yMin, yMax]); hold(hAxCastVar, 'on'); - cMap = colormap(hAxCastVar, jet(lenSampleData)); + cMap = colormap(hAxCastVar, parula(lenSampleData)); cMap = flipud(cMap); end diff --git a/Graph/lineMooring1DVar.m b/Graph/lineMooring1DVar.m index f0e5a9c23..b74a0488f 100644 --- a/Graph/lineMooring1DVar.m +++ b/Graph/lineMooring1DVar.m @@ -18,32 +18,32 @@ function lineMooring1DVar(sample_data, varName, isQC, saveToFile, exportDir) % % -% Copyright (c) 2009, eMarine Information Infrastructure (eMII) and Integrated +% Copyright (c) 2009, eMarine Information Infrastructure (eMII) and Integrated % Marine Observing System (IMOS). % All rights reserved. -% -% Redistribution and use in source and binary forms, with or without +% +% Redistribution and use in source and binary forms, with or without % modification, are permitted provided that the following conditions are met: -% -% * Redistributions of source code must retain the above copyright notice, +% +% * Redistributions of source code must retain the above copyright notice, % this list of conditions and the following disclaimer. -% * Redistributions in binary form must reproduce the above copyright -% notice, this list of conditions and the following disclaimer in the +% * Redistributions in binary form must reproduce the above copyright +% notice, this list of conditions and the following disclaimer in the % documentation and/or other materials provided with the distribution. -% * Neither the name of the eMII/IMOS nor the names of its contributors -% may be used to endorse or promote products derived from this software +% * Neither the name of the eMII/IMOS nor the names of its contributors +% may be used to endorse or promote products derived from this software % without specific prior written permission. -% -% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE % POSSIBILITY OF SUCH DAMAGE. % narginchk(5,5); @@ -85,7 +85,7 @@ function lineMooring1DVar(sample_data, varName, isQC, saveToFile, exportDir) iTime = getVar(sample_data{i}.dimensions, 'TIME'); iVar = getVar(sample_data{i}.variables, varName); iGood = true(size(sample_data{i}.dimensions{iTime}.data)); - + % the variable exists, is QC'd and is 1D if isQC && iVar && size(sample_data{i}.variables{iVar}.data, 2) == 1 %get time and var QC information @@ -119,6 +119,8 @@ function lineMooring1DVar(sample_data, varName, isQC, saveToFile, exportDir) initiateFigure = true; isPlottable = false; +backgroundColor = [0.75 0.75 0.75]; + for i=1:lenSampleData % instrument description if ~isempty(strtrim(sample_data{iSort(i)}.instrument)) @@ -133,13 +135,13 @@ function lineMooring1DVar(sample_data, varName, isQC, saveToFile, exportDir) end instrumentDesc{i + 1} = [strrep(instrumentDesc{i + 1}, '_', ' ') ' (' num2str(metaDepth(i)) 'm' instrumentSN ')']; - + %look for time and relevant variable iTime = getVar(sample_data{iSort(i)}.dimensions, 'TIME'); iVar = getVar(sample_data{iSort(i)}.variables, varName); if iVar > 0 && size(sample_data{iSort(i)}.variables{iVar}.data, 2) == 1 && ... % we're only plotting 1D variables but no current - all(~strcmpi(sample_data{iSort(i)}.variables{iVar}.name, {'UCUR', 'VCUR', 'WCUR', 'CDIR', 'CSPD', 'VEL1', 'VEL2', 'VEL3'})) + all(~strcmpi(sample_data{iSort(i)}.variables{iVar}.name, {'UCUR', 'VCUR', 'WCUR', 'CDIR', 'CSPD', 'VEL1', 'VEL2', 'VEL3'})) if initiateFigure fileName = genIMOSFileName(sample_data{iSort(i)}, 'png'); visible = 'on'; @@ -159,16 +161,37 @@ function lineMooring1DVar(sample_data, varName, isQC, saveToFile, exportDir) set(hAxMooringVar, 'XLim', [xMin, xMax]); hold(hAxMooringVar, 'on'); - % reverse the colorbar as we want surface in red and bottom in blue - cMap = colormap(hAxMooringVar, jet(lenSampleData)); + % dummy entry for first entry in legend + hLineVar(1) = plot(0, 0, 'Color', backgroundColor, 'Visible', 'off'); % color grey same as background (invisible) + + % set data cursor mode custom display + dcm_obj = datacursormode(hFigMooringVar); + set(dcm_obj, 'UpdateFcn', {@customDcm, sample_data}); + + % set zoom datetick update + datetick(hAxMooringVar, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); + zoomH = zoom(hFigMooringVar); + panH = pan(hFigMooringVar); + set(zoomH,'ActionPostCallback',{@zoomDateTick, hAxMooringVar}); + set(panH,'ActionPostCallback',{@zoomDateTick, hAxMooringVar}); + + try + defaultColormapFh = str2func(readProperty('visualQC.defaultColormap')); + cMap = colormap(hAxMooringVar, defaultColormapFh(lenSampleData)); + catch e + cMap = colormap(hAxMooringVar, parula(lenSampleData)); + end + % reverse the colorbar as we want surface instruments with warmer colors cMap = flipud(cMap); initiateFigure = false; end if strcmpi(varName, 'DEPTH') - hLineVar(1) = line([xMin, xMax], [metaDepth(i), metaDepth(i)], ... + hNominalDepth = line([xMin, xMax], [metaDepth(i), metaDepth(i)], ... 'Color', 'black'); + % turn off legend entry for this plot + set(get(get(hNominalDepth,'Annotation'),'LegendInformation'),'IconDisplayStyle','off'); end iGood = true(size(sample_data{iSort(i)}.variables{iVar}.data)); @@ -193,12 +216,16 @@ function lineMooring1DVar(sample_data, varName, isQC, saveToFile, exportDir) dataVar = sample_data{iSort(i)}.variables{iVar}.data; dataVar(~iGood) = NaN; - + hLineVar(i + 1) = line(xLine, ... dataVar, ... 'Color', cMap(i, :), ... 'LineStyle', lineStyle{mod(i, lenLineStyle)+1}); - + userData.idx = iSort(i); + userData.xName = 'TIME'; + userData.yName = varName; + set(hLineVar(i + 1), 'UserData', userData); + clear('userData'); % Let's redefine properties after pcolor to make sure grid lines appear % above color data and XTick and XTickLabel haven't changed set(hAxMooringVar, ... @@ -208,7 +235,7 @@ function lineMooring1DVar(sample_data, varName, isQC, saveToFile, exportDir) 'Layer', 'top'); % set background to be grey - set(hAxMooringVar, 'Color', [0.75 0.75 0.75]) + set(hAxMooringVar, 'Color', backgroundColor) end end end @@ -230,45 +257,35 @@ function lineMooring1DVar(sample_data, varName, isQC, saveToFile, exportDir) datetick(hAxMooringVar, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); - % we try to split the legend in two location horizontally - nLine = length(hLineVar); - if nLine > 2 - nLine1 = ceil(nLine/2); - - hLegend(1) = multipleLegend(hAxMooringVar, ... - hLineVar(1:nLine1), instrumentDesc(1:nLine1), ... - 'Interpreter', 'none', ... - 'Location', 'SouthOutside'); - hLegend(2) = multipleLegend(hAxMooringVar, ... - hLineVar(nLine1+1:nLine), instrumentDesc(nLine1+1:nLine), ... - 'Interpreter', 'none', ... - 'Location', 'SouthOutside'); - - posAx = get(hAxMooringVar, 'Position'); - - pos1 = get(hLegend(1), 'Position'); - pos2 = get(hLegend(2), 'Position'); - maxWidth = max(pos1(3), pos2(3)); - - set(hLegend(1), 'Position', [posAx(1), pos1(2), pos1(3), pos1(4)]); - set(hLegend(2), 'Position', [posAx(3) - maxWidth/2, pos1(2), pos2(3), pos2(4)]); - - % set position on legends above modifies position of axis so we - % re-initialise it - set(hAxMooringVar, 'Position', posAx); + % we try to split the legend, maximum 3 columns + fontSizeAx = get(hAxMooringVar,'FontSize'); + fontSizeLb = get(get(hAxMooringVar,'XLabel'),'FontSize'); + xscale = 0.9; + if numel(instrumentDesc) < 4 + nCols = 1; + elseif numel(instrumentDesc) < 8 + nCols = 2; else - hLegend = legend(hAxMooringVar, ... - hLineVar, instrumentDesc, ... - 'Interpreter', 'none', ... - 'Location', 'SouthOutside'); - - % unfortunately we need to do this hack so that we have consistency with - % the case above - posAx = get(hAxMooringVar, 'Position'); - set(hAxMooringVar, 'Position', posAx); + nCols = 3; + fontSizeAx = fontSizeAx - 1; + xscale = 0.75; + end + hYBuffer = 1.1 * (2*(fontSizeAx + fontSizeLb)); + hLegend = legendflex(hAxMooringVar, instrumentDesc,... + 'anchor', [6 2], ... + 'buffer', [0 -hYBuffer], ... + 'ncol', nCols,... + 'FontSize', fontSizeAx,... + 'xscale',xscale); + posAx = get(hAxMooringVar, 'Position'); + set(hLegend, 'Units', 'Normalized', 'color', backgroundColor); + posLh = get(hLegend, 'Position'); + if posLh(2) < 0 + set(hLegend, 'Position',[posLh(1), abs(posLh(2)), posLh(3), posLh(4)]); + set(hAxMooringVar, 'Position',[posAx(1), posAx(2)+2*abs(posLh(2)), posAx(3), posAx(4)-2*abs(posLh(2))]); + else + set(hAxMooringVar, 'Position',[posAx(1), posAx(2)+abs(posLh(2)), posAx(3), posAx(4)-abs(posLh(2))]); end - -% set(hLegend, 'Box', 'off', 'Color', 'none'); if saveToFile % ensure the printed version is the same whatever the screen used. @@ -290,4 +307,67 @@ function lineMooring1DVar(sample_data, varName, isQC, saveToFile, exportDir) end end +%% + function datacursorText = customDcm(~, event_obj, sample_data) + %customDatacursorText : custom data tip display + + % Display the position of the data cursor + % obj Currently not used (empty) + % event_obj Handle to event object + % output_txt Data cursor text string (string or cell array of strings). + % event_obj + % xVarName, yVarName, zVarName : x, y, z (coloured by variable) names, + + dataIndex = get(event_obj,'DataIndex'); + posClic = get(event_obj,'Position'); + + p=get(event_obj,'Target'); + userData = get(p, 'UserData'); + + xName = userData.xName; + yName = userData.yName; + + sam = sample_data{userData.idx}; + + ixVar = getVar(sam.dimensions, xName); + if ixVar ~= 0 + xUnits = sam.dimensions{ixVar}.units; + else + % generalized case pass in a variable instead of a dimension + ixVar = getVar(sam.variables, xName); + xUnits = sam.variables{ixVar}.units; + end + + iyVar = getVar(sam.dimensions, yName); + if iyVar ~= 0 + yUnits = sam.dimensions{iyVar}.units; + else + % generalized case pass in a variable instead of a dimension + iyVar = getVar(sam.variables, yName); + yUnits = sam.variables{iyVar}.units; + end + + if strcmp(xName, 'TIME') + xStr = datestr(posClic(1),'yyyy-mm-dd HH:MM:SS.FFF'); + else + xStr = [num2str(posClic(1)) ' ' xUnits]; + end + + if strcmp(yName, 'TIME') + yStr = datestr(posClic(2),'yyyy-mm-dd HH:MM:SS.FFF'); + else + yStr = [num2str(posClic(2)) ' (' yUnits ')']; %num2str(posClic(2),4) + end + + datacursorText = {get(p,'DisplayName'),... + [xName ': ' xStr],... + [yName ': ' yStr]}; + %datacursorText{end+1} = ['FileName: ',get(p,'Tag')]; + end + +%% + function zoomDateTick(obj,event_obj,hAx) + datetick(hAx,'x','dd-mm-yy HH:MM:SS','keeplimits') + end + end \ No newline at end of file diff --git a/Graph/lineMooring2DVarSection.m b/Graph/lineMooring2DVarSection.m index 6c7107a1e..8789a3777 100644 --- a/Graph/lineMooring2DVarSection.m +++ b/Graph/lineMooring2DVarSection.m @@ -115,6 +115,8 @@ function lineMooring2DVarSection(sample_data, varName, timeValue, isQC, saveToFi dimTitle = imosParameters(dimName, 'long_name'); dimUnit = imosParameters(dimName, 'uom'); +backgroundColor = [0.75 0.75 0.75]; + if iVar > 0 if initiateFigure fileName = genIMOSFileName(sample_data, 'png'); @@ -136,6 +138,9 @@ function lineMooring2DVarSection(sample_data, varName, timeValue, isQC, saveToFi hold(hAxCastVar, 'on'); + % dummy entry for first entry in legend + hLineVar(1) = plot(0, 0, 'o', 'color', backgroundColor, 'Visible', 'off'); % color grey same as background (invisible) + yLine = sample_data.dimensions{i2Ddim}.data; dataVar = sample_data.variables{iVar}.data(iX); @@ -242,7 +247,7 @@ function lineMooring2DVarSection(sample_data, varName, timeValue, isQC, saveToFi 'Layer', 'top'); % set background to be grey - set(hAxCastVar, 'Color', [0.75 0.75 0.75]) + set(hAxCastVar, 'Color', backgroundColor) end if ~initiateFigure @@ -254,12 +259,12 @@ function lineMooring2DVarSection(sample_data, varName, timeValue, isQC, saveToFi hLineVar = [hLineVar; hLineFlag]; instrumentDesc = [instrumentDesc; flagDesc]; - + % Matlab >R2015 legend entries for data which are not plotted + % will be shown with reduced opacity hLegend = legend(hAxCastVar, ... - hLineVar, instrumentDesc, ... + hLineVar, regexprep(instrumentDesc,'_','\_'), ... 'Interpreter', 'none', ... 'Location', 'SouthOutside'); - % set(hLegend, 'Box', 'off', 'Color', 'none'); end diff --git a/Graph/pcolorMooring2DVar.m b/Graph/pcolorMooring2DVar.m index b776efef4..bb4c50d53 100644 --- a/Graph/pcolorMooring2DVar.m +++ b/Graph/pcolorMooring2DVar.m @@ -116,10 +116,10 @@ function pcolorMooring2DVar(sample_data, varName, isQC, saveToFile, exportDir) cMap = 'rkbwr'; cType = 'direction'; case {'CSPD'} % [0; oo[ paremeters - cMap = 'jet'; + cMap = 'parula'; cType = 'positiveFromZero'; otherwise - cMap = 'jet'; + cMap = 'parula'; cType = ''; end diff --git a/Graph/scatterMooring1DVarAgainstDepth.m b/Graph/scatterMooring1DVarAgainstDepth.m index ede728a66..89f936ed1 100644 --- a/Graph/scatterMooring1DVarAgainstDepth.m +++ b/Graph/scatterMooring1DVarAgainstDepth.m @@ -18,32 +18,32 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, % % -% Copyright (c) 2009, eMarine Information Infrastructure (eMII) and Integrated +% Copyright (c) 2009, eMarine Information Infrastructure (eMII) and Integrated % Marine Observing System (IMOS). % All rights reserved. -% -% Redistribution and use in source and binary forms, with or without +% +% Redistribution and use in source and binary forms, with or without % modification, are permitted provided that the following conditions are met: -% -% * Redistributions of source code must retain the above copyright notice, +% +% * Redistributions of source code must retain the above copyright notice, % this list of conditions and the following disclaimer. -% * Redistributions in binary form must reproduce the above copyright -% notice, this list of conditions and the following disclaimer in the +% * Redistributions in binary form must reproduce the above copyright +% notice, this list of conditions and the following disclaimer in the % documentation and/or other materials provided with the distribution. -% * Neither the name of the eMII/IMOS nor the names of its contributors -% may be used to endorse or promote products derived from this software +% * Neither the name of the eMII/IMOS nor the names of its contributors +% may be used to endorse or promote products derived from this software % without specific prior written permission. -% -% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE % POSSIBILITY OF SUCH DAMAGE. % narginchk(5,5); @@ -67,6 +67,7 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, xResolution = monitorRec(:, 3)-monitorRec(:, 1); iBigMonitor = xResolution == max(xResolution); if sum(iBigMonitor)==2, iBigMonitor(2) = false; end % in case exactly same monitors + title = [sample_data{1}.deployment_code ' mooring''s instruments ' stringQC '''d good ' varTitle]; %sort instruments by depth @@ -83,19 +84,19 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, metaDepth(i) = NaN; end iTime = getVar(sample_data{i}.dimensions, 'TIME'); - iVar = getVar(sample_data{i}.variables, varName); + izVar = getVar(sample_data{i}.variables, varName); iGood = true(size(sample_data{i}.dimensions{iTime}.data)); - + % the variable exists, is QC'd and is 1D - if isQC && iVar && size(sample_data{i}.variables{iVar}.data, 2) == 1 + if isQC && izVar && size(sample_data{i}.variables{izVar}.data, 2) == 1 %get time and var QC information timeFlags = sample_data{i}.dimensions{iTime}.flags; - varFlags = sample_data{i}.variables{iVar}.flags; + varFlags = sample_data{i}.variables{izVar}.flags; iGood = (timeFlags == 1 | timeFlags == 2) & (varFlags == 1 | varFlags == 2); end - if iVar + if izVar if all(~iGood) continue; end @@ -125,29 +126,31 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, %look for time and relevant variable iTime = getVar(sample_data{iSort(i)}.dimensions, 'TIME'); iDepth = getVar(sample_data{iSort(i)}.variables, 'DEPTH'); - iVar = getVar(sample_data{iSort(i)}.variables, varName); + izVar = getVar(sample_data{iSort(i)}.variables, varName); - if iVar > 0 && iDepth > 0 && ... - size(sample_data{iSort(i)}.variables{iVar}.data, 2) == 1 && ... % we're only plotting 1D variables with depth variable but no current - all(~strcmpi(sample_data{iSort(i)}.variables{iVar}.name, {'UCUR', 'VCUR', 'WCUR', 'CDIR', 'CSPD', 'VEL1', 'VEL2', 'VEL3'})) - iGood = true(size(sample_data{iSort(i)}.variables{iVar}.data)); + if izVar > 0 && iDepth > 0 && ... + size(sample_data{iSort(i)}.variables{izVar}.data, 2) == 1 && ... % we're only plotting 1D variables with depth variable but no current + all(~strcmpi(sample_data{iSort(i)}.variables{izVar}.name, {'UCUR', 'VCUR', 'WCUR', 'CDIR', 'CSPD', 'VEL1', 'VEL2', 'VEL3'})) + iGood = true(size(sample_data{iSort(i)}.variables{izVar}.data)); if isQC %get time, depth and var QC information timeFlags = sample_data{iSort(i)}.dimensions{iTime}.flags; depthFlags = sample_data{iSort(i)}.variables{iDepth}.flags; - varFlags = sample_data{iSort(i)}.variables{iVar}.flags; + varFlags = sample_data{iSort(i)}.variables{izVar}.flags; iGood = (timeFlags == 1 | timeFlags == 2) & (varFlags == 1 | varFlags == 2) & (depthFlags == 1 | depthFlags == 2); end if any(iGood) isPlottable(i) = true; - minClim = min(minClim, min(sample_data{iSort(i)}.variables{iVar}.data(iGood))); - maxClim = max(maxClim, max(sample_data{iSort(i)}.variables{iVar}.data(iGood))); + minClim = min(minClim, min(sample_data{iSort(i)}.variables{izVar}.data(iGood))); + maxClim = max(maxClim, max(sample_data{iSort(i)}.variables{izVar}.data(iGood))); end end end +backgroundColor = [0.75 0.75 0.75]; + if any(isPlottable) % collect visualQC config try @@ -155,7 +158,7 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, catch e %#ok fastScatter = true; end - + initiateFigure = true; for i=1:lenSampleData % instrument description @@ -175,7 +178,7 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, %look for time and relevant variable iTime = getVar(sample_data{iSort(i)}.dimensions, 'TIME'); iDepth = getVar(sample_data{iSort(i)}.variables, 'DEPTH'); - iVar = getVar(sample_data{iSort(i)}.variables, varName); + izVar = getVar(sample_data{iSort(i)}.variables, varName); if isPlottable(i) if initiateFigure @@ -197,21 +200,44 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, set(hAxMooringVar, 'XLim', [xMin, xMax]); hold(hAxMooringVar, 'on'); + % dummy entry for first entry in legend + hScatterVar(1) = plot(0, 0, 'Color', backgroundColor, 'Visible', 'off'); % color grey same as background (invisible) + + % set data cursor mode custom display + dcm_obj = datacursormode(hFigMooringVar); + set(dcm_obj, 'UpdateFcn', {@customDcm, sample_data}, 'SnapToDataVertex','on'); + + % set zoom datetick update + datetick(hAxMooringVar, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); + zoomH = zoom(hFigMooringVar); + panH = pan(hFigMooringVar); + set(zoomH,'ActionPostCallback',{@zoomDateTick, hAxMooringVar}); + set(panH,'ActionPostCallback',{@zoomDateTick, hAxMooringVar}); + + try + nColors = str2double(readProperty('visualQC.ncolors')); + defaultColormapFh = str2func(readProperty('visualQC.defaultColormap')); + cMap = colormap(hAxMooringVar, defaultColormapFh(nColors)); + catch e + nColors = 64; + cMap = colormap(hAxMooringVar, parula(nColors)); + end + hCBar = colorbar('peer', hAxMooringVar); set(get(hCBar, 'Title'), 'String', [varName ' (' varUnit ')'], 'Interpreter', 'none'); initiateFigure = false; end - iGood = true(size(sample_data{iSort(i)}.variables{iVar}.data)); + iGood = true(size(sample_data{iSort(i)}.variables{izVar}.data)); iGoodDepth = iGood; if isQC %get time, depth and var QC information timeFlags = sample_data{iSort(i)}.dimensions{iTime}.flags; depthFlags = sample_data{iSort(i)}.variables{iDepth}.flags; - varFlags = sample_data{iSort(i)}.variables{iVar}.flags; - varValues = sample_data{iSort(i)}.variables{iVar}.data; + varFlags = sample_data{iSort(i)}.variables{izVar}.flags; + varValues = sample_data{iSort(i)}.variables{izVar}.data; iGood = (timeFlags == 1 | timeFlags == 2) & ... (varFlags == 1 | varFlags == 2) & ... @@ -228,6 +254,13 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, depth(~iGoodDepth) = metaDepth(i); depth = depth(iGood); + % data for customDcm + userData.idx = iSort(i); + userData.xName = 'TIME'; + userData.yName = 'DEPTH'; + userData.zName = varName; + userData.iGood = iGood; + if fastScatter % for performance, we use plot (1 single handle object % returned) rather than scatter (as many handles returned as @@ -240,21 +273,26 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, % first to last) of the total points given. We choose an ordering from % centre to both ends of colorbar in order to keep extreme colors visible % though. + h = plotclr(hAxMooringVar, ... sample_data{iSort(i)}.dimensions{iTime}.data(iGood), ... depth, ... - sample_data{iSort(i)}.variables{iVar}.data(iGood), ... + sample_data{iSort(i)}.variables{izVar}.data(iGood), ... markerStyle{mod(i, lenMarkerStyle)+1}, ... - [minClim maxClim]); + [minClim maxClim], 'DisplayName', instrumentDesc{i+1},'UserData', userData); else - h = scatter(hAxMooringVar, ... - sample_data{iSort(i)}.dimensions{iTime}.data(iGood), ... - depth, ... - 5, ... - sample_data{iSort(i)}.variables{iVar}.data(iGood), ... - markerStyle{mod(i, lenMarkerStyle)+1}, ... - MarkerFaceColor, 'none'); + % faster than scatter, but legend requires adjusting + h = fastScatterMesh( hAxMooringVar,... + sample_data{iSort(i)}.dimensions{iTime}.data(iGood),... + depth,... + sample_data{iSort(i)}.variables{izVar}.data(iGood),... + [minClim maxClim],... + 'Marker',markerStyle{mod(i, lenMarkerStyle)+1},... + 'MarkerSize',2.5,... + 'DisplayName',[markerStyle{mod(i, lenMarkerStyle)+1} ' ' instrumentDesc{i+1}],... + 'UserData', userData); end + clear('userData'); if ~isempty(h), hScatterVar(i + 1) = h; end @@ -267,12 +305,19 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, 'Layer', 'top'); % set background to be grey - set(hAxMooringVar, 'Color', [0.75 0.75 0.75]) + set(hAxMooringVar, 'Color', backgroundColor) end % we plot the instrument nominal depth - hScatterVar(1) = line([xMin, xMax], [metaDepth(i), metaDepth(i)], ... + hNominalDepth = line([xMin, xMax], [metaDepth(i), metaDepth(i)], ... 'Color', 'black'); + % turn off legend entry for this plot + set(get(get(hNominalDepth,'Annotation'),'LegendInformation'),'IconDisplayStyle','off'); + % with 'HitTest' == 'off' plot should not be selectable but + % just in case set idx = NaN for customDcm + userData.idx = NaN; + set(hNominalDepth, 'UserData', userData, 'HitTest', 'off'); + clear('userData'); end end else @@ -294,30 +339,48 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, % we try to split the legend in two location horizontally nLine = length(hScatterVar); + fontSizeAx = get(hAxMooringVar,'FontSize'); + fontSizeLb = get(get(hAxMooringVar,'XLabel'),'FontSize'); + xscale = 0.9; if nLine > 2 - nLine1 = ceil(nLine/2); - - hLegend(1) = multipleLegend(hAxMooringVar, ... - hScatterVar(1:nLine1), instrumentDesc(1:nLine1), ... - 'Interpreter', 'none', ... - 'Location', 'SouthOutside'); - hLegend(2) = multipleLegend(hAxMooringVar, ... - hScatterVar(nLine1+1:nLine), instrumentDesc(nLine1+1:nLine), ... - 'Interpreter', 'none', ... - 'Location', 'SouthOutside'); - + if numel(instrumentDesc) < 4 + nCols = 1; + elseif numel(instrumentDesc) < 8 + nCols = 2; + else + nCols = 3; + fontSizeAx = fontSizeAx - 1; + xscale = 0.75; + end + hYBuffer = 1.1 * (2*(fontSizeAx + fontSizeLb)); + hLegend = legendflex(hAxMooringVar, instrumentDesc, ... + 'anchor', [6 2], ... + 'buffer', [0 -hYBuffer], ... + 'ncol', nCols,... + 'FontSize', fontSizeAx, ... + 'xscale', xscale); + entries = get(hLegend,'children'); + % if used mesh for scatter plot then have to clean up legend + % entries + for ii = 1:numel(entries) + if strcmpi(get(entries(ii),'Type'),'patch') + XData = get(entries(ii),'XData'); + YData = get(entries(ii),'YData'); + %CData = get(entries(ii),'CData'); + set(entries(ii),'XData',repmat(mean(XData),size(XData))) + set(entries(ii),'YData',repmat(mean(YData),size(XData))) + %set(entries(ii),'CData',CData(1)) + end + end posAx = get(hAxMooringVar, 'Position'); - - pos1 = get(hLegend(1), 'Position'); - pos2 = get(hLegend(2), 'Position'); - maxWidth = max(pos1(3), pos2(3)); - - set(hLegend(1), 'Position', [posAx(1), pos1(2), pos1(3), pos1(4)]); - set(hLegend(2), 'Position', [posAx(3) - maxWidth/2, pos1(2), pos2(3), pos2(4)]); - - % set position on legends above modifies position of axis so we - % re-initialise it - set(hAxMooringVar, 'Position', posAx); + set(hLegend, 'Units', 'Normalized', 'color', backgroundColor); + posLh = get(hLegend, 'Position'); + if posLh(2) < 0 + set(hLegend, 'Position',[posLh(1), abs(posLh(2)), posLh(3), posLh(4)]); + set(hAxMooringVar, 'Position',[posAx(1), posAx(2)+2*abs(posLh(2)), posAx(3), posAx(4)-2*abs(posLh(2))]); + else + set(hAxMooringVar, 'Position',[posAx(1), posAx(2)+abs(posLh(2)), posAx(3), posAx(4)-abs(posLh(2))]); + end else % doesn't make sense to continue and export to file since seing a % scatter plot in depth only helps to analyse the data in its @@ -326,7 +389,7 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, return; end -% set(hLegend, 'Box', 'off', 'Color', 'none'); + % set(hLegend, 'Box', 'off', 'Color', 'none'); if saveToFile % ensure the printed version is the same whatever the screen used. @@ -335,7 +398,7 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, % preserve the color scheme set(hFigMooringVar, 'InvertHardcopy', 'off'); - + fileName = strrep(fileName, '_PARAM_', ['_', varName, '_']); % IMOS_[sub-facility_code]_[site_code]_FV01_[deployment_code]_[PLOT-TYPE]_[PARAM]_C-[creation_date].png fileName = strrep(fileName, '_PLOT-TYPE_', '_SCATTER_'); @@ -348,4 +411,119 @@ function scatterMooring1DVarAgainstDepth(sample_data, varName, isQC, saveToFile, end end + function datacursorText = customDcm(~, event_obj, sample_data) + %customDcm : custom data tip display for 1D Var Against Depth plot + % + % Display the position of the data cursor + % obj Currently not used (empty) + % event_obj Handle to event object + % datacursorText Data cursor text string (string or cell array of strings). + % sample_data : the data plotted, since only good data is plotted require + % iGood passed in on UserData + % + % NOTES + % - the multiple try catch blocks are there to trap and modifications of + % the UserData field (by say an external function called before entry into + % customDcm + + dataIndex = get(event_obj,'DataIndex'); + posClic = get(event_obj,'Position'); + + target_obj=get(event_obj,'Target'); + userData = get(target_obj, 'UserData'); + + % somehow selected nominal depth line plot + if isnan(userData.idx), return; end + + sam = sample_data{userData.idx}; + + xName = userData.xName; + yName = userData.yName; + zName = userData.zName; + + try + dStr = get(target_obj,'DisplayName'); + catch + dStr = 'UNKNOWN'; + end + + try + % generalized case pass in a variable instead of a dimension + ixVar = getVar(sam.dimensions, xName); + if ixVar ~= 0 + xUnits = sam.dimensions{ixVar}.units; + else + ixVar = getVar(sam.variables, xName); + xUnits = sam.variables{ixVar}.units; + end + if strcmp(xName, 'TIME') + xStr = datestr(posClic(1),'yyyy-mm-dd HH:MM:SS.FFF'); + else + xStr = [num2str(posClic(1)) ' ' xUnits]; + end + catch + xStr = 'NO DATA'; + end + + try + % generalized case pass in a variable instead of a dimension + iyVar = getVar(sam.dimensions, yName); + if iyVar ~= 0 + yUnits = sam.dimensions{iyVar}.units; + else + iyVar = getVar(sam.variables, yName); + yUnits = sam.variables{iyVar}.units; + end + if strcmp(yName, 'TIME') + yStr = datestr(posClic(2),'yyyy-mm-dd HH:MM:SS.FFF'); + else + yStr = [num2str(posClic(2)) ' ' yUnits]; %num2str(posClic(2),4) + end + catch + yStr = 'NO DATA'; + end + + try + % generalized case pass in a variable instead of a dimension + izVar = getVar(sam.dimensions, zName); + if izVar ~= 0 + zUnits = sam.dimensions{izVar}.units; + zData = sam.dimensions{izVar}.data(userData.iGood); + else + izVar = getVar(sam.variables, zName); + zUnits = sam.variables{izVar}.units; + zData = sam.variables{izVar}.data(userData.iGood); + end + iTime = getVar(sam.dimensions, 'TIME'); + timeData = sam.dimensions{iTime}.data(userData.iGood); + idx = find(abs(timeData-posClic(1))', '<', 'p', 'h'}; lenMarkerStyle = length(markerStyle); @@ -178,6 +186,8 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, end end +backgroundColor = [0.75 0.75 0.75]; + if any(isPlottable) % collect visualQC config try @@ -187,8 +197,8 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, end % define cMap, cLim and cType per parameter - switch varName - case {'UCUR', 'VCUR', 'WCUR', 'VEL1', 'VEL2', 'VEL3'} % 0 centred parameters + switch varName(1:4) + case {'UCUR', 'VCUR', 'WCUR', 'ECUR', 'VEL1', 'VEL2', 'VEL3'} % 0 centred parameters cMap = 'r_b'; cType = 'centeredOnZero'; CLim = [-yLimMax yLimMax]; @@ -197,11 +207,25 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, cType = 'direction'; CLim = [0 360]; case {'CSPD'} % [0; oo[ paremeters - cMap = 'jet'; + try + nColors = str2double(readProperty('visualQC.ncolors')); + defaultColormapFh = str2func(readProperty('visualQC.defaultColormap')); + cMap = defaultColormapFh(nColors); + catch e + nColors = 64; + cMap = parula(nColors); + end cType = 'positiveFromZero'; CLim = [0 yLimMax]; otherwise - cMap = 'jet'; + try + nColors = str2double(readProperty('visualQC.ncolors')); + defaultColormapFh = str2func(readProperty('visualQC.defaultColormap')); + cMap = defaultColormapFh(nColors); + catch e + nColors = 64; + cMap = parula(nColors); + end cType = ''; CLim = [yLimMin yLimMax]; end @@ -249,6 +273,20 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, set(hAxMooringVar, 'XLim', [xMin, xMax]); hold(hAxMooringVar, 'on'); + % dummy entry for first entry in legend + hScatterVar(1) = plot(0, 0, 'Color', backgroundColor, 'Visible', 'off'); % color grey same as background (invisible) + + % set data cursor mode custom display + dcm_obj = datacursormode(hFigMooringVar); + set(dcm_obj, 'UpdateFcn', {@customDcm, sample_data}, 'SnapToDataVertex','on'); + + % set zoom datetick update + datetick(hAxMooringVar, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); + zoomH = zoom(hFigMooringVar); + panH = pan(hFigMooringVar); + set(zoomH,'ActionPostCallback',{@zoomDateTick, hAxMooringVar}); + set(panH,'ActionPostCallback',{@zoomDateTick, hAxMooringVar}); + hCBar = colorbar('peer', hAxMooringVar, 'YLim', CLim); colormap(hAxMooringVar, cMap); set(hAxMooringVar, 'CLim', CLim); @@ -282,8 +320,8 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, iGoodHeight = any(iGood, 1); nGoodHeight = sum(iGoodHeight); -% nGoodHeight = nGoodHeight + 1; -% iGoodHeight(nGoodHeight) = 1; + % nGoodHeight = nGoodHeight + 1; + % iGoodHeight(nGoodHeight) = 1; if all(all(~iGood)) && isQC fprintf('%s\n', ['Warning : in ' sample_data{iSort(i)}.toolbox_input_file ... @@ -307,6 +345,13 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, dataVar(~iGood) = NaN; for j=1:nGoodHeight + % data for customDcm + userData.idx = iSort(i); + userData.jHeight = j; + userData.xName = 'TIME'; + userData.yName = 'DEPTH'; + userData.zName = varName; + if fastScatter % for performance, we use plot (1 single handle object % returned) rather than scatter (as many handles returned as @@ -324,16 +369,22 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, dataDepth - yScatter(j), ... dataVar(:, j), ... markerStyle{mod(i, lenMarkerStyle)+1}, ... - CLim); + CLim,... + 'DisplayName',instrumentDesc{i+1},... + 'UserData', userData); else - h = scatter(hAxMooringVar, ... - xScatter, ... - dataDepth - yScatter(j), ... - 5, ... - dataVar(:, j), ... - markerStyle{mod(i, lenMarkerStyle)+1}, ... - MarkerFaceColor, 'none'); + % faster than scatter, but legend requires adjusting + h = fastScatterMesh( hAxMooringVar,... + xScatter,... + dataDepth - yScatter(j),... + dataVar(:, j),... + CLim,... + 'Marker',markerStyle{mod(i, lenMarkerStyle)+1},... + 'MarkerSize',2.5,... + 'DisplayName',instrumentDesc{i+1},... + 'UserData', userData); end + clear('userData'); if ~isempty(h), hScatterVar(i + 1) = h; end end @@ -347,12 +398,19 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, 'Layer', 'top'); % set background to be grey - set(hAxMooringVar, 'Color', [0.75 0.75 0.75]) + set(hAxMooringVar, 'Color', backgroundColor) end % we plot the instrument nominal depth - hScatterVar(1) = line([xMin, xMax], [metaDepth(i), metaDepth(i)], ... + hNominalDepth = line([xMin, xMax], [metaDepth(i), metaDepth(i)], ... 'Color', 'black'); + % turn off legend entry for this plot + set(get(get(hNominalDepth,'Annotation'),'LegendInformation'),'IconDisplayStyle','off'); + % with 'HitTest' == 'off' plot should not be selectable but + % just in case set idx = NaN for customDcm + userData.idx = NaN; + set(hNominalDepth, 'UserData', userData, 'HitTest', 'off'); + clear('userData'); end end else @@ -368,45 +426,50 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, datetick(hAxMooringVar, 'x', 'dd-mm-yy HH:MM:SS', 'keepticks'); - % we try to split the legend in two location horizontally - nLine = length(hScatterVar); - if nLine > 2 - nLine1 = ceil(nLine/2); - - hLegend(1) = multipleLegend(hAxMooringVar, ... - hScatterVar(1:nLine1), instrumentDesc(1:nLine1), ... - 'Interpreter', 'none', ... - 'Location', 'SouthOutside'); - hLegend(2) = multipleLegend(hAxMooringVar, ... - hScatterVar(nLine1+1:nLine), instrumentDesc(nLine1+1:nLine), ... - 'Interpreter', 'none', ... - 'Location', 'SouthOutside'); - - posAx = get(hAxMooringVar, 'Position'); - - pos1 = get(hLegend(1), 'Position'); - pos2 = get(hLegend(2), 'Position'); - maxWidth = max(pos1(3), pos2(3)); - - set(hLegend(1), 'Position', [posAx(1), pos1(2), pos1(3), pos1(4)]); - set(hLegend(2), 'Position', [posAx(3) - maxWidth/2, pos1(2), pos2(3), pos2(4)]); - - % set position on legends above modifies position of axis so we - % re-initialise it - set(hAxMooringVar, 'Position', posAx); + % we try to split the legend horizontally, max 3 columns + fontSizeAx = get(hAxMooringVar,'FontSize'); + fontSizeLb = get(get(hAxMooringVar,'XLabel'),'FontSize'); + xscale = 0.9; + if numel(instrumentDesc) < 4 + nCols = 1; + elseif numel(instrumentDesc) < 8 + nCols = 2; else - hLegend = legend(hAxMooringVar, ... - hScatterVar, instrumentDesc, ... - 'Interpreter', 'none', ... - 'Location', 'SouthOutside'); - - % unfortunately we need to do this hack so that we have consistency with - % the case above - posAx = get(hAxMooringVar, 'Position'); - set(hAxMooringVar, 'Position', posAx); + nCols = 3; + fontSizeAx = fontSizeAx - 1; + xscale = 0.75; + end + hYBuffer = 1.1 * (2*(fontSizeAx + fontSizeLb)); + hLegend = legendflex(hAxMooringVar,instrumentDesc,... + 'anchor', [6 2], ... + 'buffer', [0 -hYBuffer], ... + 'ncol', nCols,... + 'FontSize', fontSizeAx,'xscale',xscale); + + % if used mesh for scatter plot then have to clean up legend + % entries + entries = get(hLegend,'children'); + for ii = 1:numel(entries) + if strcmpi(get(entries(ii),'Type'),'patch') + XData = get(entries(ii),'XData'); + YData = get(entries(ii),'YData'); + %CData = get(entries(ii),'CData'); + set(entries(ii),'XData',repmat(mean(XData),size(XData))) + set(entries(ii),'YData',repmat(mean(YData),size(XData))) + %set(entries(ii),'CData',CData(1)) + end + end + posAx = get(hAxMooringVar, 'Position'); + set(hLegend, 'Units', 'Normalized', 'color', backgroundColor) + posLh = get(hLegend, 'Position'); + if posLh(2) < 0 + set(hLegend, 'Position',[posLh(1), abs(posLh(2)), posLh(3), posLh(4)]); + set(hAxMooringVar, 'Position',[posAx(1), posAx(2)+2*abs(posLh(2)), posAx(3), posAx(4)-2*abs(posLh(2))]); + else + set(hAxMooringVar, 'Position',[posAx(1), posAx(2)+abs(posLh(2)), posAx(3), posAx(4)-abs(posLh(2))]); end -% set(hLegend, 'Box', 'off', 'Color', 'none'); + % set(hLegend, 'Box', 'off', 'Color', 'none'); if saveToFile % ensure the printed version is the same whatever the screen used. @@ -415,7 +478,7 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, % preserve the color scheme set(hFigMooringVar, 'InvertHardcopy', 'off'); - + fileName = strrep(fileName, '_PARAM_', ['_', varName, '_']); % IMOS_[sub-facility_code]_[site_code]_FV01_[deployment_code]_[PLOT-TYPE]_[PARAM]_C-[creation_date].png fileName = strrep(fileName, '_PLOT-TYPE_', '_SCATTER_'); @@ -428,4 +491,122 @@ function scatterMooring2DVarAgainstDepth(sample_data, varName, isQC, saveToFile, end end -end \ No newline at end of file +%% + function datacursorText = customDcm(~, event_obj, sample_data) + %customDcm : custom data tip display for 1D Var Against Depth plot + % + % Display the position of the data cursor + % obj Currently not used (empty) + % event_obj Handle to event object + % datacursorText Data cursor text string (string or cell array of strings). + % sample_data : the data plotted, since only good data is plotted require + % iGood passed in on UserData + % + % NOTES + % - the multiple try catch blocks are there to trap and modifications of + % the UserData field (by say an external function called before entry into + % customDcm + + dataIndex = get(event_obj,'DataIndex'); + posClic = get(event_obj,'Position'); + + target_obj=get(event_obj,'Target'); + userData = get(target_obj, 'UserData'); + + % somehow selected nominal depth line plot + if isnan(userData.idx), return; end + + sam = sample_data{userData.idx}; + + xName = userData.xName; + yName = userData.yName; + zName = userData.zName; + + try + dStr = get(target_obj,'DisplayName'); + catch + dStr = 'UNKNOWN'; + end + + try + % generalized case pass in a variable instead of a dimension + ixVar = getVar(sam.dimensions, xName); + if ixVar ~= 0 + xUnits = sam.dimensions{ixVar}.units; + else + ixVar = getVar(sam.variables, xName); + xUnits = sam.variables{ixVar}.units; + end + if strcmp(xName, 'TIME') + xStr = datestr(posClic(1),'yyyy-mm-dd HH:MM:SS.FFF'); + else + xStr = [num2str(posClic(1)) ' ' xUnits]; + end + catch + xStr = 'NO DATA'; + end + + try + % generalized case pass in a variable instead of a dimension + iyVar = getVar(sam.dimensions, yName); + if iyVar ~= 0 + yUnits = sam.dimensions{iyVar}.units; + else + iyVar = getVar(sam.variables, yName); + yUnits = sam.variables{iyVar}.units; + end + if strcmp(yName, 'TIME') + yStr = datestr(posClic(2),'yyyy-mm-dd HH:MM:SS.FFF'); + else + yStr = [num2str(posClic(2)) ' ' yUnits]; %num2str(posClic(2),4) + end + catch + yStr = 'NO DATA'; + end + + try + % generalized case pass in a variable instead of a dimension + izVar = getVar(sam.dimensions, zName); + if izVar ~= 0 + zUnits = sam.dimensions{izVar}.units; + zData = sam.dimensions{izVar}.data; + else + izVar = getVar(sam.variables, zName); + zUnits = sam.variables{izVar}.units; + zData = sam.variables{izVar}.data; + end + + iTime = getVar(sam.dimensions, 'TIME'); + timeData = sam.dimensions{iTime}.data; + idx = find(abs(timeData-posClic(1)) get the value + if ~strcmp(M{n},'#') + + % Modify the "Units" property of H + set(h,'units',unit{n}); + % Modify the "Units" property of HREF + set(href,'units',unit{n}); + % Get the current "Position" vector of H + temp=get(h,'position'); + % Get the current "Position" vector of HREF + if strcmp(get(href, 'type'), 'root') % HREF is the Root object (no 'Position' property) + temp_href=get(href,'screensize'); %%% Should be safe here ! + else temp_href=get(href,'position'); + end + % Get and store the specified field from the "Position" vector + % If HREF is specified and is not the parent of H, flag_href=1 else flag_href=0 + pos(n)=temp(n)-temp_href(n)*flag_href; + + end + +end + +% Check for compact output format +if strcmpi(opt,'compact') + pos(isnan(pos))=[]; +end + +% Restore the unit of the graphics object H +set(h,'units',current_unit); +% Restore the unit of the reference object HREF +set(href,'units',current_ref_unit); \ No newline at end of file diff --git a/Util/legendflex.m b/Util/legendflex.m new file mode 100644 index 000000000..3eed973a7 --- /dev/null +++ b/Util/legendflex.m @@ -0,0 +1,827 @@ +function varargout = legendflex(varargin) +%LEGENDFLEX Creates a more flexible legend +% +% legendflex(M, param1, val1, ...) +% legendflex(h, M, param1, val1, ...) +% [legend_h,object_h,plot_h,text_str] = legendflex(...) +% +% This offers a more flexible version of the legend command. It offers a +% different method of positioning the legend, as well as options to: +% +% - organize legend text and symbols in a grid with a specified number of +% rows and/or columns +% - rescale the horizontal space used by each legend symbol +% - create multiple legends for the same axis +% - add a title to the legend within the legend box +% +% Unlike in the default legend command, where the legend is positioned +% relative to the labeled objects' parent axis according to one of 16 +% location strings, this function positions the legend based on two anchor +% points (one on either the figure or a child object of a figure, and one +% on the legend itself) and a buffer (or offset) between these two anchor +% points. The anchor points refer to the corners and centers of each +% side of the box surrounding the reference object and the legend itself; +% they can be refered to either as numbers (1-8, clockwise from northwest +% corner) or strings ('nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'). The +% position of the legend is determined by these two points and the distance +% between them, defined in the 'buffer' variable, which by default is +% measured in pixels. So the combination of +% +% (..., 'ref', gca, 'anchor', [3 3], 'buffer', [-10 -10]) +% +% means that you want the northeast corner of the current axis to be +% aligned with the northeast corner of the legend, but with the legend +% shifted 10 pixels to the left and down. +% +% This method of positioning can be particularly useful when labeling a +% figure that includes many subplots that share a common color scheme, +% where the "best" location for a legend is not necessarily within the +% bounds of an axis. Unlike the legend command, the axes in the figure are +% never resized (and it is up to the user to check that the legend fits on +% the figure in the specified location). In addition to being easier than +% manually positioning a legend, this function updates the legend location +% when the figure is resized, preserving the desired alignment. The +% following anchor/buffer combinations, when used with the default +% reference and a buffer unit of pixels, approximately replicate the +% typical legend locations: +% +% Specifier Anchor Buffer +% +% north [2 2] [ 0 -10] +% south [6 6] [ 0 10] +% east [4 4] [-10 0] +% west [8 8] [ 10 0] +% northeast [3 3] [-10 -10] +% northwest [1 1] [ 10 -10] +% southeast [5 5] [-10 10] +% southwest [7 7] [ 10 10] +% northoutside* [2 6] [ 0 10] +% southoutside* [6 2] [ 0 -10] +% eastoutside* [3 8] [ 10 0] +% westoutside* [8 3] [-10 0] +% northeastoutside* [3 1] [ 10 0] +% northwestoutside* [1 3] [-10 0] +% southeastoutside* [5 7] [ 10 0] +% southwestoutside* [7 5] [-10 0] *placed outside axis rather +% than resizing plot box +% +% This function should support all types of plot objects. +% +% Updates to labeled line and patch properties should be reflected in the +% legend. In pre-R2014b versions of Matlab (those that use the old +% non-object graphics handles), properties of more complex legend labels, +% such as contours, quivers, bars, etc.) will also be synced to the legend; +% however, at this time, the code doesn't update properties for anything +% other than lines and patches in R2014b+ (haven't found a good way to +% listen for changes to the properties of the other graphics object types). +% +% A note on resizing: This function assigns a resize function to the parent +% figure to maintain the position of the legend (in terms of anchor +% location and buffer) as the figure size changes. If you manually resize +% the legend, this function will respect changes to height, width, and +% units (though I don't recommend changing the units to 'normalized', as +% this can cause the text and symbols to overflow the legend box on +% resize). It will not respect manual repositioning when resizing, since +% it assumes you want to maintain the anchor/buffer prescription used to +% create it. Overall, I've tried to make this resize as unobtrusive as +% possible; if your figure already has a resize function at the time you +% apply it, that behavior is inherited, with the legend-resize called +% afterward. If you plan to further modify the figure's resize function +% post-legendflex and want to maintain repositioning of the legends, +% retrieve the resize function via hfun = get(hfig, 'ResizeFcn'), pass it +% to the new resize function, and invoke it via feval(oldfun, h, ed), where +% h and ed are the default variables passed by a callback function. +% +% Input variables: +% +% M: cell array of strings, labels for legend +% +% h: handle of axis or handle(s) of object(s) to be labeled. If +% this is an axis handle, all children of the axis will be +% included in the legend. If not included, current axis is +% used. +% +% Optional input variables (passed as parameter/value pairs): [default] +% +% ncol: number of columns, or 0 to indicate as many as necessary +% given the # of labeled objects [1 if nrow is 0, 0 +% otherwise] +% +% nrow: number of rows, or 0 to indicate as many as necessary +% given the # of labeled objects [0] +% +% ref: handle of object used to position the legend. This can be +% either a figure or a child object of a figure (and does not +% need to relate in any way to the objects being labeled). +% If not included, the reference will be to the axis that a +% normal legend would be associated with (usually the parent +% axis of the labeled objects, unless objects from multiple +% axes are passed, in which case it's the parent object of +% the first labeled object). +% +% anchor: 1 x 2 array specifying which points of the reference object +% and new legend, respectively, to anchor to each other. +% Anchor points can be described using either numbers (in a 1 +% x 2 double array) or directional strings (in a 1 x 2 cell +% array) as follows: +% 1: 'nw' upper left corner +% 2: 'n' center of top edge +% 3: 'ne' upper right corner +% 4: 'e' center of right edge +% 5: 'se' bottom right corner +% 6: 's' center of bottom edge +% 7: 'sw' bottom left corner +% 8: 'w' center of left edge +% +% [[3 3], i.e. {'ne' 'ne'}] +% +% buffer: 1 x 2 array of horizontal and vertical distance, +% respectively, from the reference anchor point to the legend +% anchor point. Distance is measured in units specified by +% bufferunit. [[-10 -10]] +% +% bufferunit: unit for buffer distance. Note that this property only +% affects the units used to position the legend, not the +% units for the legend itself (which is always a fixed size, +% based on the space needed to encapsulate the specified +% symbols and text). The 'normalized' units are normalized +% to size of the figure. ['pixels'] +% +% box: 'on' or 'off', specifies whether to enclose legend objects +% in a box ['on'] +% +% xscale: scalar value indicating scale factor to apply to the width +% required by each symbol, relative to the size used by +% legend. For example, 0.5 will shorten the lines/patches by +% half. [1] +% +% title: A title string to be added inside the legend box, centered, +% above all legend entries. This can be either a string or a +% cell array of strings; the latter will produce a multi-line +% title. If empty, no title is added. [''] +% +% padding: 1 x 3 array, pixel spacing added to beginning of each +% column (before symbol), between symbol and text, and after +% text, respectively. Usually, the default provides the +% spacing typical of a regular legend, but occassionally the +% extent properties wrap a little too close to text, making +% things look crowded; in these cases you can try unsquishing +% things via this parameter. [2 1 1] +% +% In addition to these legendflex-specific parameters, this function will +% accept any parameter accepted by the original legend function (e.g. +% font properties) except 'location', 'boxon', 'boxoff', or 'hide'. +% +% Output variables: +% +% legend_h: handle of the legend axis. It is not linked to an axis or +% graphics objects in the same way as a Matlab legend. +% However, on figure resize, all properties of the legend +% objects are checked for changes, so adjusting the figure +% size can re-link the legend to the labeled objects after +% you have made changes to those objects. +% +% object_h: handles of the line, patch, and text graphics objects +% created in the legend +% +% plot_h: handles of the lines and other objects labeled in this +% legend +% +% text_str: cell array of the text strings used in the legend +% +% +% Example: +% +% % Replicating an example from legend.m: +% +% figure; +% b = bar(rand(10,5),'stacked'); colormap(summer); hold on +% x = plot(1:10,5*rand(10,1),'marker','square','markersize',12,... +% 'markeredgecolor','y','markerfacecolor',[.6 0 .6],... +% 'linestyle','-','color','r','linewidth',2); hold off +% lbl = {'Carrots','Peas','Peppers','Green Beans','Cucumbers','Eggplant'}; +% +% % Rather than covering up data or resizing the axis, let's squeeze the +% % legend into the margin at the top of the figure; +% +% legendflex([b,x], lbl, 'ref', gcf, ... +% 'anchor', {'n','n'}, ... +% 'buffer',[0 0], ... +% 'nrow',2, ... +% 'fontsize',8); + +% Copyright 2011-2014 Kelly Kearney + +% Detemine whether HG2 is in use + +hg2flag = ~verLessThan('matlab', '8.4.0'); + +%------------------- +% Parse input +%------------------- +% +% allinput = varargin; % Save for callback later +% +% islegin = false(size(varargin)); + +% First inputs must be either: +% (M, ...) +% (h, M, ...) + +narginchk(1,Inf); + +% Split input into the variables that will be passed to legend (handles and +% labels) and everything else + +handlepassed = all(ishandle(varargin{1})); % for HG1/HG2 + +if handlepassed + legin = varargin(1:2); + if ~iscell(legin{2}) || ~all(cellfun(@ischar, legin{2})) + error('Legend labels must be a cell array of strings'); + end + pv = varargin(3:end); +else + legin = varargin(1); + if ~iscell(legin{1}) || ~all(cellfun(@ischar, legin{1})) + if isnumeric(legin{1}) + error('Unable to parse input 1; check that handle(s) exist'); + else + error('Legend labels must be a cell array of strings'); + end + end + pv = varargin(2:end); +end + +% Parse my optional properties + +if hg2flag + defref = gobjects(0); +else + defref = NaN; +end + +p = inputParser; +p.addParamValue('xscale', 1, @(x) validateattributes(x, {'numeric'}, {'nonnegative','scalar'})); +p.addParamValue('ncol', 0, @(x) validateattributes(x, {'numeric'}, {'scalar', 'integer'})); +p.addParamValue('nrow', 0, @(x) validateattributes(x, {'numeric'}, {'scalar', 'integer'})); +p.addParamValue('ref', defref, @(x) validateattributes(x, {'numeric','handle'}, {'scalar'})); +p.addParamValue('anchor', [3 3], @(x) validateattributes(x, {'numeric','cell'}, {'size', [1 2]})); +p.addParamValue('buffer', [-10 -10], @(x) validateattributes(x, {'numeric'}, {'size', [1 2]})); +p.addParamValue('bufferunit', 'pixels', @(x) validateattributes(x, {'char'}, {})); +p.addParamValue('box', 'on', @(x) validateattributes(x, {'char'}, {})); +p.addParamValue('title', '', @(x) validateattributes(x, {'char','cell'}, {})); +p.addParamValue('padding', [2 1 1], @(x) validateattributes(x, {'numeric'}, {'nonnegative', 'size', [1 3]})); + +p.KeepUnmatched = true; + +p.parse(pv{:}); +Opt = p.Results; + +% Any parameters that don't match mine are assumed to be a legend property. +% If not, legend will handle the error when I call it. + +Extra = p.Unmatched; +extra = [fieldnames(Extra) struct2cell(Extra)]; +extra = extra'; + +% Validate that units and box inputs are correct + +validatestring(Opt.bufferunit, {'pixels','normalized','inches','centimeters','points','characters'}, 'legendflex', 'bufferunit'); +validatestring(Opt.box, {'on', 'off'}, 'legendflex', 'box'); + +% Translate anchor strings to numbers, if necessary + +if iscell(Opt.anchor) + [blah, Opt.anchor] = ismember(Opt.anchor, {'nw','n','ne','e','se','s','sw','w'}); + if ~all(blah) + error('Anchor must be 1 x 2 cell array of strings: n, e, s, w, ne, nw, se, sw'); + end +else + validateattributes(Opt.anchor, {'numeric'}, {'integer', '<=', 8}, 'legendflex', 'anchor'); +end + +% Create a temporary legend to get all the objects + +S = warning('off', 'MATLAB:legend:PlotEmpty'); +[h.leg, h.obj, h.labeledobj, h.textstr] = legend(legin{:}, extra{:}, 'location', 'northeast'); +nobj = length(h.labeledobj); +warning(S); + +if nobj == 0 + warning('Plot empty; no legend created'); + return +end + +% There's a bug in R2014b-R2015a that causes rendering issues if a contour +% object is included in a legend and legend is called with more than one +% output. For some reason, the rendering issues disappear only if the +% contour object(s) is listed last in the legend. So for now, my +% workaround for this is to change the order of the legend labels as +% necessary. + +iscont = strcmp(get(h.labeledobj, 'type'), 'contour'); +cbugflag = ~verLessThan('matlab', '8.4.0') && any(iscont); +if cbugflag + + if length(legin) == 1 + legin = {h.labeledobj legin{1}}; + end + + delete(h.leg); + + [srt, isrt] = sort(iscont); + legin{1} = legin{1}(isrt); + legin{2} = legin{2}(isrt); + + [h.leg, h.obj, h.labeledobj, h.textstr] = legend(legin{:}, extra{:}, 'location', 'northeast'); + +end + +% # rows and columns + +if (Opt.ncol == 0) && (Opt.nrow == 0) + Opt.ncol = 1; + Opt.nrow = nobj; +elseif (Opt.ncol == 0) + Opt.ncol = ceil(nobj./Opt.nrow); +elseif (Opt.nrow == 0) + Opt.nrow = ceil(nobj./Opt.ncol); +end +if Opt.ncol*Opt.nrow < nobj + error('Number of legend entries greater than specified grid allows; change ncol and/or nrow'); +end + +% Reference object + +if hg2flag + + if isempty(Opt.ref) + + if all(ishandle(legin{1})) + tmp = ancestor(legin{1}, 'axes'); + if iscell(tmp) + Opt.ref = tmp{1}; + else + Opt.ref = tmp(1); + end + else + Opt.ref = gca; + end + + end +else + if isnan(Opt.ref) + tmp = get(h.leg, 'UserData'); + Opt.ref = tmp.PlotHandle; + end +end +if ~ishandle(Opt.ref) + error('Input ref must be a graphics handle'); +end + +% Box + +Opt.box = strcmpi('on', Opt.box); + +% Convert units to getpos abbreviations + +unittable = {... + 'px' 'Pixels' + 'nz' 'Normalized' + 'in' 'Inches' + 'cm' 'Centimeters' + 'pt' 'Points' + 'ch' 'Characters'}; +Opt.bufunit = unittable{strcmpi(unittable(:,2),Opt.bufferunit),1}; + +% Check for title + +addtitle = ~isempty(Opt.title); + +%------------------- +% New placement of +% everything in +% legend +%------------------- + +% Determine parent figure + +figh = ancestor(Opt.ref, 'figure'); +currax = get(figh, 'currentaxes'); + +% Calculate row height + +legpospx = getpos(h.leg, 'px'); + +% rowHeight = legpospx(4)/nobj; +vmarginNm = 0.275/nobj; +vmarginPx = legpospx(4) * vmarginNm; + +rowHeightNm = (1 - vmarginNm)/nobj; +rowHeight = rowHeightNm .* legpospx(4); + +% Determine width needed for each text string + +if nobj == 1 + textExtent = get(h.obj(1:nobj), 'Extent'); +else + textExtent = cell2mat(get(h.obj(1:nobj), 'Extent')); +end +textWidthPx = textExtent(:,3) .* legpospx(3); +textHeightPx = textExtent(:,4) .* legpospx(4); +textWidthNm = textExtent(:,3); + +% Calculate horizontal space needed for symbols + +symbolWidthPx = textExtent(1,1) .* legpospx(3) * Opt.xscale; +symbolWidthNm = textExtent(1,1); + +% Calculate column width needed for 2px-symbol-1px-text-1px + +colWidth = zeros(Opt.ncol*Opt.nrow,1); +colWidth(1:nobj) = textWidthPx + symbolWidthPx + sum(Opt.padding); +colWidth = reshape(colWidth, Opt.nrow, Opt.ncol); +colWidth = max(colWidth,[],1); + +% If title is added, figure out how much space it will need + +if addtitle + textProps = {'FontAngle','FontName','FontSize','FontUnits','FontWeight','Interpreter'}; + textVals = get(h.obj(1), textProps); + ttlprops = [textProps; textVals]; + + fpos = getpos(figh, 'px'); + figtmp = figure('units','pixels','position',[0 0 fpos(3:4)],'visible','off'); + axes('parent',figtmp,'position',[0 0 1 1],'xlim',[0 fpos(3)],'ylim',[0 fpos(4)]); + tmp = text(0,0,Opt.title, ttlprops{:}, 'horiz', 'left', 'vert', 'bottom'); + ttlex = get(tmp, 'extent'); + ttlwidth = ceil(ttlex(3)) + 4; % Add a little padding + ttlheight = ceil(ttlex(4)); + + if ttlwidth > sum(colWidth) + colWidth(end) = colWidth(end) + (ttlwidth-sum(colWidth)); + end + close(figtmp); +end + +% Locate bottom left corner of each legend symbol, text box, and title + +xsymbnew = [0 cumsum(colWidth(1:end-1))]+Opt.padding(1); +ysymbnew = (rowHeight*Opt.nrow + vmarginPx)-(1:Opt.nrow)*rowHeight; +[xsymbnew, ysymbnew] = meshgrid(xsymbnew, ysymbnew); +xsymbnew = xsymbnew(1:nobj); +ysymbnew = ysymbnew(1:nobj); + +xtext = xsymbnew + Opt.padding(2) + symbolWidthPx; +ytext = ysymbnew;% + 1; + +xsymbold = zeros(nobj,1); +ysymbold = 1 - (1/nobj)*(1:nobj); + +wnewleg = sum(colWidth); +hnewleg = rowHeight*Opt.nrow + vmarginPx; + +if addtitle + xttl = wnewleg/2; + yttl = hnewleg; + hnewleg = hnewleg + ttlheight; +end + +% Get legend position in bufferunit and translate to pixels + +legpos = positionleg(Opt.ref, wnewleg, hnewleg, Opt.anchor, Opt.buffer, Opt.bufunit); +tmpax = axes('units', Opt.bufferunit, 'position', legpos,'visible','off'); +legpos = getpos(tmpax, 'px'); +delete(tmpax); + +%------------------- +% Create legend +%------------------- + +% Create the legend axis + +hnew.leg = axes('units', 'pixels', ... + 'position', legpos, ... + 'xlim', [0 legpos(3)], ... + 'ylim', [0 legpos(4)], ... + 'xtick', [], ... + 'ytick', [], ... + 'box', 'on', ... + 'parent', figh); + +% Copy the text strings to the new legend + +textProps = {'FontAngle','FontName','FontSize','FontUnits','FontWeight','Interpreter','HorizontalAlignment','VerticalAlignment'}; +textVals = get(h.obj(1:nobj), textProps); + +if hg2flag + hnew.obj = gobjects(size(h.obj)); +else + hnew.obj = zeros(size(h.obj)); +end +for it = 1:nobj + props = [textProps; textVals(it,:)]; + hnew.obj(it) = text(xtext(it), ytext(it), h.textstr{it}, props{:}, ... + 'horizontalalignment', 'left', ... + 'verticalalignment', 'bottom'); +end + +% Copy the symbols to the new legend + +nsymbol = length(h.obj) - nobj; + +for ii = 1:nsymbol + + if strcmp(get(h.obj(nobj+ii), 'type'), 'hggroup') + + tag = get(h.obj(nobj+ii),'Tag'); + if ~isempty(tag) + [blah, idx] = ismember(tag,h.textstr); + end + + chld = findall(h.obj(nobj+ii), 'type', 'line', '-or', 'type', 'patch'); + for ic = 1:length(chld) + xy = get(chld(ic), {'xdata', 'ydata'}); + + xnorm = xy{1}./symbolWidthNm; + ynorm = (xy{2}- (1-idx*rowHeightNm))./rowHeightNm; + + xnew = xnorm * symbolWidthPx + xsymbnew(idx); + ynew = ynorm * rowHeight + ysymbnew(idx); + + set(chld(ic), 'xdata', xnew, 'ydata', ynew); + end + + hnew.obj(nobj+ii) = copyobj(h.obj(nobj+ii), hnew.leg); + + else + + hnew.obj(nobj+ii) = copyobj(h.obj(nobj+ii), hnew.leg); + + tag = get(h.obj(nobj+ii),'Tag'); + if ~isempty(tag) % assumes empty tags indicate repetition of previous tag (true pre-2014b) + [blah, idx] = ismember(tag,h.textstr); + end + + xy = get(h.obj(nobj+ii), {'xdata', 'ydata'}); + + xnorm = xy{1}./symbolWidthNm; + ynorm = (xy{2}- (1-idx*rowHeightNm))./rowHeightNm; + + xnew = xnorm * symbolWidthPx + xsymbnew(idx); + ynew = ynorm * rowHeight + ysymbnew(idx); + + set(hnew.obj(nobj+ii), 'xdata', xnew, 'ydata', ynew); + + end + +end + +% Add title + +if addtitle + text(xttl, yttl, Opt.title, ttlprops{:}, 'horiz', 'center', 'vert', 'bottom'); +end + +% Add box or hide axis + +if Opt.box + set(hnew.leg, 'box', 'on'); +else + set(hnew.leg, 'visible', 'off'); +end + +% Delete the temporary legend + +delete(h.leg); + +% Return focus to previously-current axis + +set(figh, 'currentaxes', currax); +drawnow; % Not sure why this is necessary for the currentaxes to take effect, but it is + +%------------------- +% Callbacks and +% listeners +%------------------- + +% Save some relevant variables in the new legend axis's application data + +Lf.ref = Opt.ref; +Lf.w = wnewleg; +Lf.h = hnewleg; +Lf.anchor = Opt.anchor; +Lf.buffer = Opt.buffer; +Lf.bufunit = Opt.bufunit; +Lf.bufferunit = Opt.bufferunit; +Lf.plotobj = h.labeledobj; +Lf.legobj = hnew.obj; + +setappdata(hnew.leg, 'legflex', Lf); + +% Resize listeners + +addlistener(hnew.leg, 'Position', 'PostSet', @(src,evt) updatelegappdata(src,evt,hnew.leg)); +if hg2flag && strcmp(Lf.ref.Type, 'figure') + addlistener(Lf.ref, 'SizeChanged', @(src,evt) updatelegpos(src,evt,hnew.leg)); +else + addlistener(Lf.ref, 'Position', 'PostSet', @(src,evt) updatelegpos(src,evt,hnew.leg)); +end +rsz = get(figh, 'ResizeFcn'); +if isempty(rsz) % No previous resize function + set(figh, 'ResizeFcn', @updatelegfigresize); +else + if ~iscell(rsz) + rsz = {rsz}; + end + hasprev = cellfun(@(x) isequal(x, @updatelegfigresize), rsz); + if ~hasprev + rsz = {rsz{:} @updatelegfigresize}; + set(figh, 'ResizeFcn', {@wrapper, rsz}); + end +end + +% Run the resync function if anything changes with the labeled objects + +objwatch = findall(h.labeledobj, 'type', 'line', '-or', 'type', 'patch'); + +for ii = 1:length(objwatch) + switch lower(get(objwatch(ii), 'type')) + case 'line' + triggerprops = {'Color','LineStyle','LineWidth','Marker','MarkerSize','MarkerEdgeColor','MarkerFaceColor'}; + addlistener(objwatch(ii), triggerprops, 'PostSet', @(h,ed) resyncprops(h,ed,hnew.leg)); + case 'patch' + triggerprops = {'CData','CDataMapping','EdgeAlpha','EdgeColor','FaceAlpha','FaceColor','LineStyle','LineWidth','Marker','MarkerEdgeColor','MarkerFaceColor','MarkerSize'}; + addlistener(objwatch(ii), triggerprops, 'PostSet', @(h,ed) resyncprops(h,ed,hnew.leg)); + end +end + + +%------------------- +% Output +%------------------- + +out = {hnew.leg, hnew.obj, h.labeledobj, h.textstr}; +varargout = out(1:nargout); + + +%***** Subfunctions ***** + +%------------------------ +% Position new legend +%------------------------ + +function legpos = positionleg(href, w, h, anchor, buffer, bufunit) +% ap: position vector for reference object +% lp: position vector for legend + +if strcmp(get(href, 'type'), 'figure') + tmp = axes('parent', href,'position', [0 0 1 1],'visible','off'); + pos = getpos(tmp, bufunit); + delete(tmp); +else + pos = getpos(href, bufunit); +end + +htmp = axes('units', 'pixels', 'position', [0 0 w h], 'visible','off'); +lpos = getpos(htmp, bufunit); +delete(htmp); +w = lpos(3); +h = lpos(4); + +% Find anchor locations on reference object + +refxy = [... + pos(1) pos(2)+pos(4) + pos(1)+pos(3)/2 pos(2)+pos(4) + pos(1)+pos(3) pos(2)+pos(4) + pos(1)+pos(3) pos(2)+pos(4)/2 + pos(1)+pos(3) pos(2) + pos(1)+pos(3)/2 pos(2) + pos(1) pos(2) + pos(1) pos(2)+pos(4)/2]; + +% How bottom left relates to each anchor point + +shift = [... + 0 -h + -w/2 -h + -w -h + -w -h/2 + -w 0 + -w/2 0 + 0 0 + 0 -h/2]; + +% Legend location + +corner = refxy(anchor(1),:) + buffer + shift(anchor(2),:); +legpos = [corner w h]; + +%------------------------ +% Listener functions +%------------------------ + +% If user manually resizes the legend, update the app data + +function updatelegappdata(src, evt, legax) +if ishandle(legax) + Lf = getappdata(legax, 'legflex'); + pos = getpos(legax, 'px'); + Lf.w = pos(3); + Lf.h = pos(4); + setappdata(legax, 'legflex', Lf); +end +% If reference object moves or resizes, reposition the legend appropriately + +function updatelegpos(src, evt, legax) +if ishandle(legax) + Lf = getappdata(legax, 'legflex'); + legpos = positionleg(Lf.ref, Lf.w, Lf.h, Lf.anchor, Lf.buffer, Lf.bufunit); + set(legax, 'Units', Lf.bufferunit, 'Position', legpos); +end + +% Since figure resize can change axis size without actually triggering a +% listener, force this + +function updatelegfigresize(src, evt) + +allax = findall(src, 'type', 'axes'); +for ii = 1:length(allax) + isleg = ~isempty(getappdata(allax(ii), 'legflex')); + if ~isleg + pos = get(allax(ii), 'Position'); + set(allax(ii), 'Position', pos); % No change, just trigger PostSet + end +end + +% If plotted object changes, resync with legend + +function resyncprops(src, evt, legax) + +if ishandle(legax) % In case it's been deleted + + Lf = getappdata(legax, 'legflex'); + + str = cellstr(num2str((1:length(Lf.plotobj))')); + [htmp.leg, htmp.obj, htmp.labeledobj, htmp.textstr] = legend(Lf.plotobj, str); + + objtype = get(Lf.legobj, 'type'); + isline = strcmp(objtype, 'line'); + ispatch = strcmp(objtype, 'patch'); + ishg = strcmp(objtype, 'hggroup'); + hgidx = find(ishg); + + lobj = [Lf.legobj(isline) htmp.obj(isline)]; + pobj = [Lf.legobj(ispatch) htmp.obj(ispatch)]; + + if ~isempty(hgidx) + for ih = hgidx + chldln1 = findall(Lf.legobj(ih), 'type', 'line'); + chldln2 = findall(htmp.obj(ih), 'type', 'line'); + + lobj = [lobj; [chldln1 chldln2]]; + + chldpa1 = findall(Lf.legobj(ih), 'type', 'patch'); + chldpa2 = findall(htmp.obj(ih), 'type', 'patch'); + + pobj = [pobj; [chldpa1 chldpa2]]; + + end + end + + lprops = {'color','linestyle','linewidth','marker','markersize','markeredgecolor','markerfacecolor'}; + for il = 1:size(lobj,1) + lvals = get(lobj(il,2), lprops); + pv = [lprops; lvals]; + set(lobj(il,1), pv{:}); + end + + pprops = {'cdata','cdatamapping','edgealpha','edgecolor','facealpha','facecolor','linestyle','linewidth','marker','markeredgecolor','markerfacecolor','markersize'}; + for ip = 1:size(pobj,1) + pvals = get(pobj(ip,2), pprops); + pv = [pprops; pvals]; + set(pobj(ip,1), pv{:}); + end + + cmap = colormap(htmp.leg); + colormap(legax, cmap); + + delete(htmp.leg); +end + +% Wrapper to add multiple callback functions to resize + +function wrapper(ObjH, EventData, fcnList) +for ii = 1:length(fcnList) + feval(fcnList{ii}, ObjH, EventData); +end + + + + + + + + diff --git a/Util/parula.m b/Util/parula.m new file mode 100644 index 000000000..9bc34d417 --- /dev/null +++ b/Util/parula.m @@ -0,0 +1,77 @@ +function cm_data=parula(m) + +cm = [[0.2081, 0.1663, 0.5292], + [0.2116238095, 0.1897809524, 0.5776761905], + [0.212252381, 0.2137714286, 0.6269714286], + [0.2081, 0.2386, 0.6770857143], + [0.1959047619, 0.2644571429, 0.7279], + [0.1707285714, 0.2919380952, 0.779247619], + [0.1252714286, 0.3242428571, 0.8302714286], + [0.0591333333, 0.3598333333, 0.8683333333], + [0.0116952381, 0.3875095238, 0.8819571429], + [0.0059571429, 0.4086142857, 0.8828428571], + [0.0165142857, 0.4266, 0.8786333333], + [0.032852381, 0.4430428571, 0.8719571429], + [0.0498142857, 0.4585714286, 0.8640571429], + [0.0629333333, 0.4736904762, 0.8554380952], + [0.0722666667, 0.4886666667, 0.8467], + [0.0779428571, 0.5039857143, 0.8383714286], + [0.079347619, 0.5200238095, 0.8311809524], + [0.0749428571, 0.5375428571, 0.8262714286], + [0.0640571429, 0.5569857143, 0.8239571429], + [0.0487714286, 0.5772238095, 0.8228285714], + [0.0343428571, 0.5965809524, 0.819852381], + [0.0265, 0.6137, 0.8135], + [0.0238904762, 0.6286619048, 0.8037619048], + [0.0230904762, 0.6417857143, 0.7912666667], + [0.0227714286, 0.6534857143, 0.7767571429], + [0.0266619048, 0.6641952381, 0.7607190476], + [0.0383714286, 0.6742714286, 0.743552381], + [0.0589714286, 0.6837571429, 0.7253857143], + [0.0843, 0.6928333333, 0.7061666667], + [0.1132952381, 0.7015, 0.6858571429], + [0.1452714286, 0.7097571429, 0.6646285714], + [0.1801333333, 0.7176571429, 0.6424333333], + [0.2178285714, 0.7250428571, 0.6192619048], + [0.2586428571, 0.7317142857, 0.5954285714], + [0.3021714286, 0.7376047619, 0.5711857143], + [0.3481666667, 0.7424333333, 0.5472666667], + [0.3952571429, 0.7459, 0.5244428571], + [0.4420095238, 0.7480809524, 0.5033142857], + [0.4871238095, 0.7490619048, 0.4839761905], + [0.5300285714, 0.7491142857, 0.4661142857], + [0.5708571429, 0.7485190476, 0.4493904762], + [0.609852381, 0.7473142857, 0.4336857143], + [0.6473, 0.7456, 0.4188], + [0.6834190476, 0.7434761905, 0.4044333333], + [0.7184095238, 0.7411333333, 0.3904761905], + [0.7524857143, 0.7384, 0.3768142857], + [0.7858428571, 0.7355666667, 0.3632714286], + [0.8185047619, 0.7327333333, 0.3497904762], + [0.8506571429, 0.7299, 0.3360285714], + [0.8824333333, 0.7274333333, 0.3217], + [0.9139333333, 0.7257857143, 0.3062761905], + [0.9449571429, 0.7261142857, 0.2886428571], + [0.9738952381, 0.7313952381, 0.266647619], + [0.9937714286, 0.7454571429, 0.240347619], + [0.9990428571, 0.7653142857, 0.2164142857], + [0.9955333333, 0.7860571429, 0.196652381], + [0.988, 0.8066, 0.1793666667], + [0.9788571429, 0.8271428571, 0.1633142857], + [0.9697, 0.8481380952, 0.147452381], + [0.9625857143, 0.8705142857, 0.1309], + [0.9588714286, 0.8949, 0.1132428571], + [0.9598238095, 0.9218333333, 0.0948380952], + [0.9661, 0.9514428571, 0.0755333333], + [0.9763, 0.9831, 0.0538]]; + + if nargin < 1 + cm_data = cm; +else + cm_data = zeros(m,3); + hsv=rgb2hsv(cm); + cm_data(:,1)=interp1(linspace(0,1,size(cm,1)),hsv(:,1),linspace(0,1,m)); + cm_data(:,2)=interp1(linspace(0,1,size(cm,1)),hsv(:,2),linspace(0,1,m)); + cm_data(:,3)=interp1(linspace(0,1,size(cm,1)),hsv(:,3),linspace(0,1,m)); + cm_data=hsv2rgb(cm_data); +end \ No newline at end of file diff --git a/Util/plotclr.m b/Util/plotclr.m index f5dddcde3..ef503137f 100644 --- a/Util/plotclr.m +++ b/Util/plotclr.m @@ -1,4 +1,4 @@ -function h = plotclr(hAx, x, y, v, marker, vlim) +function h = plotclr(hAx, x, y, v, marker, vlim, varargin) % plots the values of v colour coded % at the positions specified by x and y. % A colourbar is added on the right side of the figure. @@ -79,10 +79,9 @@ else iv = (v > miv+(nc-1)*clrstep) & (v <= miv+nc*clrstep); end - htmp = plot(hAx, x(iv), y(iv), marker, ... 'Color', map(nc,:), ... - 'MarkerSize', sqrt(5)); + 'MarkerSize', sqrt(5), varargin{:}); if ~isempty(htmp), h = htmp; end @@ -94,10 +93,9 @@ else iv = (v > miv+(nc-1)*clrstep) & (v <= miv+nc*clrstep); end - htmp = plot(hAx, x(iv), y(iv), marker, ... 'Color', map(nc,:), ... - 'MarkerSize', sqrt(5)); + 'MarkerSize', sqrt(5), varargin{:}); if ~isempty(htmp), h = htmp; end end diff --git a/Util/setpos.m b/Util/setpos.m new file mode 100644 index 000000000..c474036f3 --- /dev/null +++ b/Util/setpos.m @@ -0,0 +1,187 @@ +function setpos(h,fmt,href) +% SETPOS Set graphics object position in a flexible way. +% SETPOS(H,FMT) sets the position property of graphics object +% with handle H, according to FMT that can be expressed using different +% units. H must have a "Position' property. +% +% FMT is a char array containing 4 strings separated by colon or space. +% The format of each string is one of "%1c%f%2c" or "%1c%d%2c" where the +% first optional argument is "+" or "-", the second one is a number and +% the last one is two characters that specify the unit as : +% +% px for Pixels +% nz for Normalized +% in for Inches +% cm for Centimeters +% pt for Points +% ch for Characters +% [] (empty) for Current units [See get(H,'units')] +% +% For better rendering, SETPOS can be included into the "Createfcn" or +% "Resizefcn" properties of the graphical object H. +% +% Any string value of FMT can be replaced by a single '#' to keep the current +% value of the corresponding parameter. +% +% The first optional argument of FMT is used to increase ('+') or +% decrease ('-') the corresponding value. +% +% Note that SETPOS(H,FMT) works as set(H,'Position',FMT) when FMT is +% a 4 double values vector. +% +% SETPOS(H,FMT,HREF) sets the position of the graphics object H according to +% FMT, but using the position of the graphics object HREF as reference instead +% of the parent of H. HREF must be a valid handle and must have a "Position" +% property (except for the Root object). Note that this should only affect +% Left&Bottom (1st&2nd) element of the "Position" vector of H. +% +% See also GETPOS, SET, GET. + +% Author: Jérôme Briot, Matlab 6.1.0.450 (R12.1) +% Contact: dutmatlab@yahoo.fr +% Revision: 1.0 (12-Feb-2007) +% 1.1 (14-Feb-2007) Third input argument HREF added. +% Minor corrections in the help section. +% 1.2 (21-Feb-2007) Bug fixed if HREF is the Root object +% Examples removed from the help section +% Comments: +% + +% Check the number of input arguments +error(nargchk(2,3, nargin)); + +% Check if H is a graphics object handle +if ~ishandle(h) + error('First argument must be a graphic object handle in SETPOS(H,FMT)'); +end + +% If FMT is a 4x1 double vector then SETPOS works as SET(H,'Position',FMT) +if isnumeric(fmt) & numel(fmt(:))==4 + + set(h,'position',fmt) + return + +% If FMT is not a double vector, check if it's a char string +elseif ~ischar(fmt) + + error('FMT argument must be a string or a 4 elements vector in SETPOS(H,FMT)'); + +end + +if nargin==2 % SETPOS(H,FMT) + + %HREF = parent of H + href=get(h,'parent'); + +elseif nargin==3 % SETPOS(H,FMT,HREF) + + if ~ishandle(href) % Check if HREF is a valid handle + error('HREF must be a valid handle of a graphics object in SETPOS(H,FMT,HREF)') + end + +end + +flag_href=0; +% Don't use HREF position if it is the parent of H +if href~=get(h,'parent') + flag_href=1; +end + +% Extract 4 char strings from FMT +M=strread(fmt,'%s','delimiter',' ,','emptyvalue',0); + +% Store the current unit of the graphics object H +current_unit=get(h,'units'); +% Store the current unit of the reference object HREF +current_ref_unit=get(href,'units'); + +% List available units +available_units={'inches' 'centimeters' 'normalized' 'points' 'pixels' 'characters'}; + +flag=zeros(1,4); + +% Decode elements of FMT +for n=1:numel(M) + + % If FMT(n) is not a "#" + if ~strcmp(M{n},'#') + + % Check if FMT(n) is +%... or -%... + if strncmp(M{n},'+',1) + flag(n)=1; + M{n}(1)=[]; % Remove '+' char + elseif strncmp(M{n},'-',1) + flag(n)=-1; + M{n}(1)=[]; % Remove '-' char + end + + % Separate value and unit from FMT(n) + [val(n),temp_unit]=strread(M{n},'%f%s'); + + % If the unit is not specified in FMT(n) + if isempty(temp_unit) + + unit{n}=current_unit; % Set the units to the current one + + % Else check if the units paramter is valid + else idx=strcmpi(temp_unit,{'in' 'cm' 'nz' 'pt' 'px' 'ch'}); + + if ~any(idx) + error('Units must be one of "in", "cm", "nz", "pt", "px" or "ch"') + end + + unit{n}=available_units{idx}; % Set the units to one from the list + + end + + end + +end + +% Set position of H using decoded FMT +for n=1:numel(M) + + % If FMT(n) is not a "#" => value to modify + if ~strcmp(M{n},'#') + + % Modify the "Units" property of H + set(h,'units',unit{n}); + % Modify the "Units" property of HREF + set(href,'units',unit{n}); + % Get the current "Position" vector of H + position_in_unit=get(h,'position'); + % Get the current "Position" vector of HREF + if (isnumeric(href) && ~href) || (isgraphics(href) && isequal(href, groot)) % HREF is the Root object (no 'Position' property) + position_ref_unit=get(href,'screensize'); %%% Should be safe here ! + else position_ref_unit=get(href,'position'); + end + if ~flag % No "+" or "-" + + if any(n==[1 2]) + % If HREF is specified and is not the parent of H, flag_href=1 else flag_href=0 + position_in_unit(n)=val(n)+position_ref_unit(n)*flag_href; + else position_in_unit(n)=val(n); + end + + elseif any(n==[3 4]) % "+" or "-" and FMT(n) is width or height + + position_in_unit(n)=position_in_unit(n)+val(n)*flag(n); + + else % "+" or "-" and FMT(n) is left or bottom + + position_in_unit(n)=position_in_unit(n)+val(n)*flag(n); + position_in_unit(n+2)=position_in_unit(n+2)-val(n)*flag(n); + + end + + % Modify the "Position" property of H + set(h,'position',position_in_unit) + + end + +end + +% Restore the unit of the graphics object H +set(h,'units',current_unit); +% Restore the unit of the reference object HREF +set(href,'units',current_ref_unit); \ No newline at end of file diff --git a/Util/viridis.m b/Util/viridis.m new file mode 100644 index 000000000..f48e56bf2 --- /dev/null +++ b/Util/viridis.m @@ -0,0 +1,269 @@ +% http://www.mathworks.com/matlabcentral/fileexchange/51986-perceptually-uniform-colormaps/content/Colormaps/viridis.m +function cm_data=viridis(m) +cm = [[ 0.26700401, 0.00487433, 0.32941519], + [ 0.26851048, 0.00960483, 0.33542652], + [ 0.26994384, 0.01462494, 0.34137895], + [ 0.27130489, 0.01994186, 0.34726862], + [ 0.27259384, 0.02556309, 0.35309303], + [ 0.27380934, 0.03149748, 0.35885256], + [ 0.27495242, 0.03775181, 0.36454323], + [ 0.27602238, 0.04416723, 0.37016418], + [ 0.2770184 , 0.05034437, 0.37571452], + [ 0.27794143, 0.05632444, 0.38119074], + [ 0.27879067, 0.06214536, 0.38659204], + [ 0.2795655 , 0.06783587, 0.39191723], + [ 0.28026658, 0.07341724, 0.39716349], + [ 0.28089358, 0.07890703, 0.40232944], + [ 0.28144581, 0.0843197 , 0.40741404], + [ 0.28192358, 0.08966622, 0.41241521], + [ 0.28232739, 0.09495545, 0.41733086], + [ 0.28265633, 0.10019576, 0.42216032], + [ 0.28291049, 0.10539345, 0.42690202], + [ 0.28309095, 0.11055307, 0.43155375], + [ 0.28319704, 0.11567966, 0.43611482], + [ 0.28322882, 0.12077701, 0.44058404], + [ 0.28318684, 0.12584799, 0.44496 ], + [ 0.283072 , 0.13089477, 0.44924127], + [ 0.28288389, 0.13592005, 0.45342734], + [ 0.28262297, 0.14092556, 0.45751726], + [ 0.28229037, 0.14591233, 0.46150995], + [ 0.28188676, 0.15088147, 0.46540474], + [ 0.28141228, 0.15583425, 0.46920128], + [ 0.28086773, 0.16077132, 0.47289909], + [ 0.28025468, 0.16569272, 0.47649762], + [ 0.27957399, 0.17059884, 0.47999675], + [ 0.27882618, 0.1754902 , 0.48339654], + [ 0.27801236, 0.18036684, 0.48669702], + [ 0.27713437, 0.18522836, 0.48989831], + [ 0.27619376, 0.19007447, 0.49300074], + [ 0.27519116, 0.1949054 , 0.49600488], + [ 0.27412802, 0.19972086, 0.49891131], + [ 0.27300596, 0.20452049, 0.50172076], + [ 0.27182812, 0.20930306, 0.50443413], + [ 0.27059473, 0.21406899, 0.50705243], + [ 0.26930756, 0.21881782, 0.50957678], + [ 0.26796846, 0.22354911, 0.5120084 ], + [ 0.26657984, 0.2282621 , 0.5143487 ], + [ 0.2651445 , 0.23295593, 0.5165993 ], + [ 0.2636632 , 0.23763078, 0.51876163], + [ 0.26213801, 0.24228619, 0.52083736], + [ 0.26057103, 0.2469217 , 0.52282822], + [ 0.25896451, 0.25153685, 0.52473609], + [ 0.25732244, 0.2561304 , 0.52656332], + [ 0.25564519, 0.26070284, 0.52831152], + [ 0.25393498, 0.26525384, 0.52998273], + [ 0.25219404, 0.26978306, 0.53157905], + [ 0.25042462, 0.27429024, 0.53310261], + [ 0.24862899, 0.27877509, 0.53455561], + [ 0.2468114 , 0.28323662, 0.53594093], + [ 0.24497208, 0.28767547, 0.53726018], + [ 0.24311324, 0.29209154, 0.53851561], + [ 0.24123708, 0.29648471, 0.53970946], + [ 0.23934575, 0.30085494, 0.54084398], + [ 0.23744138, 0.30520222, 0.5419214 ], + [ 0.23552606, 0.30952657, 0.54294396], + [ 0.23360277, 0.31382773, 0.54391424], + [ 0.2316735 , 0.3181058 , 0.54483444], + [ 0.22973926, 0.32236127, 0.54570633], + [ 0.22780192, 0.32659432, 0.546532 ], + [ 0.2258633 , 0.33080515, 0.54731353], + [ 0.22392515, 0.334994 , 0.54805291], + [ 0.22198915, 0.33916114, 0.54875211], + [ 0.22005691, 0.34330688, 0.54941304], + [ 0.21812995, 0.34743154, 0.55003755], + [ 0.21620971, 0.35153548, 0.55062743], + [ 0.21429757, 0.35561907, 0.5511844 ], + [ 0.21239477, 0.35968273, 0.55171011], + [ 0.2105031 , 0.36372671, 0.55220646], + [ 0.20862342, 0.36775151, 0.55267486], + [ 0.20675628, 0.37175775, 0.55311653], + [ 0.20490257, 0.37574589, 0.55353282], + [ 0.20306309, 0.37971644, 0.55392505], + [ 0.20123854, 0.38366989, 0.55429441], + [ 0.1994295 , 0.38760678, 0.55464205], + [ 0.1976365 , 0.39152762, 0.55496905], + [ 0.19585993, 0.39543297, 0.55527637], + [ 0.19410009, 0.39932336, 0.55556494], + [ 0.19235719, 0.40319934, 0.55583559], + [ 0.19063135, 0.40706148, 0.55608907], + [ 0.18892259, 0.41091033, 0.55632606], + [ 0.18723083, 0.41474645, 0.55654717], + [ 0.18555593, 0.4185704 , 0.55675292], + [ 0.18389763, 0.42238275, 0.55694377], + [ 0.18225561, 0.42618405, 0.5571201 ], + [ 0.18062949, 0.42997486, 0.55728221], + [ 0.17901879, 0.43375572, 0.55743035], + [ 0.17742298, 0.4375272 , 0.55756466], + [ 0.17584148, 0.44128981, 0.55768526], + [ 0.17427363, 0.4450441 , 0.55779216], + [ 0.17271876, 0.4487906 , 0.55788532], + [ 0.17117615, 0.4525298 , 0.55796464], + [ 0.16964573, 0.45626209, 0.55803034], + [ 0.16812641, 0.45998802, 0.55808199], + [ 0.1666171 , 0.46370813, 0.55811913], + [ 0.16511703, 0.4674229 , 0.55814141], + [ 0.16362543, 0.47113278, 0.55814842], + [ 0.16214155, 0.47483821, 0.55813967], + [ 0.16066467, 0.47853961, 0.55811466], + [ 0.15919413, 0.4822374 , 0.5580728 ], + [ 0.15772933, 0.48593197, 0.55801347], + [ 0.15626973, 0.4896237 , 0.557936 ], + [ 0.15481488, 0.49331293, 0.55783967], + [ 0.15336445, 0.49700003, 0.55772371], + [ 0.1519182 , 0.50068529, 0.55758733], + [ 0.15047605, 0.50436904, 0.55742968], + [ 0.14903918, 0.50805136, 0.5572505 ], + [ 0.14760731, 0.51173263, 0.55704861], + [ 0.14618026, 0.51541316, 0.55682271], + [ 0.14475863, 0.51909319, 0.55657181], + [ 0.14334327, 0.52277292, 0.55629491], + [ 0.14193527, 0.52645254, 0.55599097], + [ 0.14053599, 0.53013219, 0.55565893], + [ 0.13914708, 0.53381201, 0.55529773], + [ 0.13777048, 0.53749213, 0.55490625], + [ 0.1364085 , 0.54117264, 0.55448339], + [ 0.13506561, 0.54485335, 0.55402906], + [ 0.13374299, 0.54853458, 0.55354108], + [ 0.13244401, 0.55221637, 0.55301828], + [ 0.13117249, 0.55589872, 0.55245948], + [ 0.1299327 , 0.55958162, 0.55186354], + [ 0.12872938, 0.56326503, 0.55122927], + [ 0.12756771, 0.56694891, 0.55055551], + [ 0.12645338, 0.57063316, 0.5498411 ], + [ 0.12539383, 0.57431754, 0.54908564], + [ 0.12439474, 0.57800205, 0.5482874 ], + [ 0.12346281, 0.58168661, 0.54744498], + [ 0.12260562, 0.58537105, 0.54655722], + [ 0.12183122, 0.58905521, 0.54562298], + [ 0.12114807, 0.59273889, 0.54464114], + [ 0.12056501, 0.59642187, 0.54361058], + [ 0.12009154, 0.60010387, 0.54253043], + [ 0.11973756, 0.60378459, 0.54139999], + [ 0.11951163, 0.60746388, 0.54021751], + [ 0.11942341, 0.61114146, 0.53898192], + [ 0.11948255, 0.61481702, 0.53769219], + [ 0.11969858, 0.61849025, 0.53634733], + [ 0.12008079, 0.62216081, 0.53494633], + [ 0.12063824, 0.62582833, 0.53348834], + [ 0.12137972, 0.62949242, 0.53197275], + [ 0.12231244, 0.63315277, 0.53039808], + [ 0.12344358, 0.63680899, 0.52876343], + [ 0.12477953, 0.64046069, 0.52706792], + [ 0.12632581, 0.64410744, 0.52531069], + [ 0.12808703, 0.64774881, 0.52349092], + [ 0.13006688, 0.65138436, 0.52160791], + [ 0.13226797, 0.65501363, 0.51966086], + [ 0.13469183, 0.65863619, 0.5176488 ], + [ 0.13733921, 0.66225157, 0.51557101], + [ 0.14020991, 0.66585927, 0.5134268 ], + [ 0.14330291, 0.66945881, 0.51121549], + [ 0.1466164 , 0.67304968, 0.50893644], + [ 0.15014782, 0.67663139, 0.5065889 ], + [ 0.15389405, 0.68020343, 0.50417217], + [ 0.15785146, 0.68376525, 0.50168574], + [ 0.16201598, 0.68731632, 0.49912906], + [ 0.1663832 , 0.69085611, 0.49650163], + [ 0.1709484 , 0.69438405, 0.49380294], + [ 0.17570671, 0.6978996 , 0.49103252], + [ 0.18065314, 0.70140222, 0.48818938], + [ 0.18578266, 0.70489133, 0.48527326], + [ 0.19109018, 0.70836635, 0.48228395], + [ 0.19657063, 0.71182668, 0.47922108], + [ 0.20221902, 0.71527175, 0.47608431], + [ 0.20803045, 0.71870095, 0.4728733 ], + [ 0.21400015, 0.72211371, 0.46958774], + [ 0.22012381, 0.72550945, 0.46622638], + [ 0.2263969 , 0.72888753, 0.46278934], + [ 0.23281498, 0.73224735, 0.45927675], + [ 0.2393739 , 0.73558828, 0.45568838], + [ 0.24606968, 0.73890972, 0.45202405], + [ 0.25289851, 0.74221104, 0.44828355], + [ 0.25985676, 0.74549162, 0.44446673], + [ 0.26694127, 0.74875084, 0.44057284], + [ 0.27414922, 0.75198807, 0.4366009 ], + [ 0.28147681, 0.75520266, 0.43255207], + [ 0.28892102, 0.75839399, 0.42842626], + [ 0.29647899, 0.76156142, 0.42422341], + [ 0.30414796, 0.76470433, 0.41994346], + [ 0.31192534, 0.76782207, 0.41558638], + [ 0.3198086 , 0.77091403, 0.41115215], + [ 0.3277958 , 0.77397953, 0.40664011], + [ 0.33588539, 0.7770179 , 0.40204917], + [ 0.34407411, 0.78002855, 0.39738103], + [ 0.35235985, 0.78301086, 0.39263579], + [ 0.36074053, 0.78596419, 0.38781353], + [ 0.3692142 , 0.78888793, 0.38291438], + [ 0.37777892, 0.79178146, 0.3779385 ], + [ 0.38643282, 0.79464415, 0.37288606], + [ 0.39517408, 0.79747541, 0.36775726], + [ 0.40400101, 0.80027461, 0.36255223], + [ 0.4129135 , 0.80304099, 0.35726893], + [ 0.42190813, 0.80577412, 0.35191009], + [ 0.43098317, 0.80847343, 0.34647607], + [ 0.44013691, 0.81113836, 0.3409673 ], + [ 0.44936763, 0.81376835, 0.33538426], + [ 0.45867362, 0.81636288, 0.32972749], + [ 0.46805314, 0.81892143, 0.32399761], + [ 0.47750446, 0.82144351, 0.31819529], + [ 0.4870258 , 0.82392862, 0.31232133], + [ 0.49661536, 0.82637633, 0.30637661], + [ 0.5062713 , 0.82878621, 0.30036211], + [ 0.51599182, 0.83115784, 0.29427888], + [ 0.52577622, 0.83349064, 0.2881265 ], + [ 0.5356211 , 0.83578452, 0.28190832], + [ 0.5455244 , 0.83803918, 0.27562602], + [ 0.55548397, 0.84025437, 0.26928147], + [ 0.5654976 , 0.8424299 , 0.26287683], + [ 0.57556297, 0.84456561, 0.25641457], + [ 0.58567772, 0.84666139, 0.24989748], + [ 0.59583934, 0.84871722, 0.24332878], + [ 0.60604528, 0.8507331 , 0.23671214], + [ 0.61629283, 0.85270912, 0.23005179], + [ 0.62657923, 0.85464543, 0.22335258], + [ 0.63690157, 0.85654226, 0.21662012], + [ 0.64725685, 0.85839991, 0.20986086], + [ 0.65764197, 0.86021878, 0.20308229], + [ 0.66805369, 0.86199932, 0.19629307], + [ 0.67848868, 0.86374211, 0.18950326], + [ 0.68894351, 0.86544779, 0.18272455], + [ 0.69941463, 0.86711711, 0.17597055], + [ 0.70989842, 0.86875092, 0.16925712], + [ 0.72039115, 0.87035015, 0.16260273], + [ 0.73088902, 0.87191584, 0.15602894], + [ 0.74138803, 0.87344918, 0.14956101], + [ 0.75188414, 0.87495143, 0.14322828], + [ 0.76237342, 0.87642392, 0.13706449], + [ 0.77285183, 0.87786808, 0.13110864], + [ 0.78331535, 0.87928545, 0.12540538], + [ 0.79375994, 0.88067763, 0.12000532], + [ 0.80418159, 0.88204632, 0.11496505], + [ 0.81457634, 0.88339329, 0.11034678], + [ 0.82494028, 0.88472036, 0.10621724], + [ 0.83526959, 0.88602943, 0.1026459 ], + [ 0.84556056, 0.88732243, 0.09970219], + [ 0.8558096 , 0.88860134, 0.09745186], + [ 0.86601325, 0.88986815, 0.09595277], + [ 0.87616824, 0.89112487, 0.09525046], + [ 0.88627146, 0.89237353, 0.09537439], + [ 0.89632002, 0.89361614, 0.09633538], + [ 0.90631121, 0.89485467, 0.09812496], + [ 0.91624212, 0.89609127, 0.1007168 ], + [ 0.92610579, 0.89732977, 0.10407067], + [ 0.93590444, 0.8985704 , 0.10813094], + [ 0.94563626, 0.899815 , 0.11283773], + [ 0.95529972, 0.90106534, 0.11812832], + [ 0.96489353, 0.90232311, 0.12394051], + [ 0.97441665, 0.90358991, 0.13021494], + [ 0.98386829, 0.90486726, 0.13689671], + [ 0.99324789, 0.90615657, 0.1439362 ]]; + +if nargin < 1 + cm_data = cm; +else + cm_data = zeros(m,3); + hsv=rgb2hsv(cm); + cm_data=interp1(linspace(0,1,size(cm,1)),hsv,linspace(0,1,m)); + cm_data=hsv2rgb(cm_data); + +end +end \ No newline at end of file diff --git a/toolboxProperties.txt b/toolboxProperties.txt index 2b2b03675..bf710d5df 100644 --- a/toolboxProperties.txt +++ b/toolboxProperties.txt @@ -91,3 +91,12 @@ saveGraph.imgType = png % visual QC plots export properties visualQC.export = true visualQC.fastScatter = true + +% default nonspecialized scalar colormap and number of colour steps +% jet | parula | viridis +visualQC.defaultColormap = parula +visualQC.ncolors = 64 + +% simple zbuffering of markers if using fallback fastScatterMesh plot (fastScatter = false) +% 'ascending', 'descending', 'triangle', 'vee', 'flat', 'parabolic', 'hamming', 'hann' +visualQC.zbuffer = triangle \ No newline at end of file From 17bb1b886d8216fb6dca88c96eb75c24d18f0baf Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Wed, 13 Apr 2016 17:45:33 +1000 Subject: [PATCH 19/21] Fix readXR420 time stamp by adding half of the averaging time period. --- Parser/readXR420.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Parser/readXR420.m b/Parser/readXR420.m index fc1e545aa..aeb7b3841 100644 --- a/Parser/readXR420.m +++ b/Parser/readXR420.m @@ -464,4 +464,8 @@ % generate time stamps from start/interval/end data.time = header.start:header.interval:header.end; data.time = data.time(1:length(samples{1}))'; + + if isfield(header, 'averaging_time_period') + data.time = data.time + (header.averaging_time_period/(24*3600))/2; %assume averaging time is always in seconds + end end From 49f472bbdef7d0d68a093f9749587f4064e84e94 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Wed, 13 Apr 2016 17:46:33 +1000 Subject: [PATCH 20/21] Need to propagate time PP from raw to qc dataset in auto mode too. --- FlowManager/autoIMOSToolbox.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowManager/autoIMOSToolbox.m b/FlowManager/autoIMOSToolbox.m index 4889ae63f..be38e602e 100644 --- a/FlowManager/autoIMOSToolbox.m +++ b/FlowManager/autoIMOSToolbox.m @@ -219,7 +219,7 @@ function autoIMOSToolbox(toolboxVersion, fieldTrip, dataDir, ppChain, qcChain, e if isempty(sample_data), continue; end raw_data = preprocessManager(sample_data, 'raw', mode, true); - qc_data = preprocessManager(sample_data, 'qc', mode, true); + qc_data = preprocessManager(raw_data, 'qc', mode, true); clear sample_data; qc_data = autoQCManager(qc_data, true); From ddfd802f6affc1acc4ae5dd8be895bee7e01d054 Mon Sep 17 00:00:00 2001 From: Guillaume Galibert Date: Thu, 14 Apr 2016 16:33:54 +1000 Subject: [PATCH 21/21] Increment toolbox version to 2.5.5 and update binaries. --- imosToolbox.m | 2 +- imosToolbox_Linux64.bin | Bin 549788 -> 582527 bytes imosToolbox_Win32.exe | Bin 703210 -> 736086 bytes imosToolbox_Win64.exe | Bin 751305 -> 784129 bytes 4 files changed, 1 insertion(+), 1 deletion(-) diff --git a/imosToolbox.m b/imosToolbox.m index 37e970d6c..728e71964 100644 --- a/imosToolbox.m +++ b/imosToolbox.m @@ -73,7 +73,7 @@ function imosToolbox(auto, varargin) end % Set current toolbox version -toolboxVersion = ['2.5.4 - ' computer]; +toolboxVersion = ['2.5.5 - ' computer]; switch auto case 'auto', autoIMOSToolbox(toolboxVersion, varargin{:}); diff --git a/imosToolbox_Linux64.bin b/imosToolbox_Linux64.bin index db417653ed13c015257e8947220c0896d514dd86..991045dc3cc792f76b5d987d2187ba1f2e23b28d 100755 GIT binary patch delta 475384 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