From 0e47b0717d54cc24ab1bcdba4d8e5eb7fb10bd0c Mon Sep 17 00:00:00 2001
From: Mark Olesen <Mark.Olesen@esi-group.com>
Date: Tue, 2 Oct 2018 14:06:34 +0200
Subject: [PATCH] ENH: vtk::seriesWriter to encapsulate writing file series
 (issue #926)

---
 applications/test/vtkSeriesWriter/Make/files  |   3 +
 .../test/vtkSeriesWriter/Make/options         |   5 +
 .../vtkSeriesWriter/Test-vtkSeriesWriter.C    | 118 +++
 applications/test/vtkSeriesWriter/file.vtm    |   4 +
 .../test/vtkSeriesWriter/file_00000679.vtm    |   4 +
 .../test/vtkSeriesWriter/file_00001025.vtm    |   4 +
 .../test/vtkSeriesWriter/file_00001234.vtm    |   4 +
 .../test/vtkSeriesWriter/file_00005680.vtm    |   4 +
 applications/test/vtkSeriesWriter/file_23.vtm |   4 +
 .../test/vtkSeriesWriter/test1.vtm.series     |  20 +
 src/fileFormats/Make/files                    |   1 +
 .../vtk/file/foamVtkSeriesWriter.C            | 753 ++++++++++++++++++
 .../vtk/file/foamVtkSeriesWriter.H            | 276 +++++++
 .../vtk/file/foamVtkSeriesWriterI.H           |  99 +++
 14 files changed, 1299 insertions(+)
 create mode 100644 applications/test/vtkSeriesWriter/Make/files
 create mode 100644 applications/test/vtkSeriesWriter/Make/options
 create mode 100644 applications/test/vtkSeriesWriter/Test-vtkSeriesWriter.C
 create mode 100644 applications/test/vtkSeriesWriter/file.vtm
 create mode 100644 applications/test/vtkSeriesWriter/file_00000679.vtm
 create mode 100644 applications/test/vtkSeriesWriter/file_00001025.vtm
 create mode 100644 applications/test/vtkSeriesWriter/file_00001234.vtm
 create mode 100644 applications/test/vtkSeriesWriter/file_00005680.vtm
 create mode 100644 applications/test/vtkSeriesWriter/file_23.vtm
 create mode 100644 applications/test/vtkSeriesWriter/test1.vtm.series
 create mode 100644 src/fileFormats/vtk/file/foamVtkSeriesWriter.C
 create mode 100644 src/fileFormats/vtk/file/foamVtkSeriesWriter.H
 create mode 100644 src/fileFormats/vtk/file/foamVtkSeriesWriterI.H

diff --git a/applications/test/vtkSeriesWriter/Make/files b/applications/test/vtkSeriesWriter/Make/files
new file mode 100644
index 00000000000..367611d7ece
--- /dev/null
+++ b/applications/test/vtkSeriesWriter/Make/files
@@ -0,0 +1,3 @@
+Test-vtkSeriesWriter.C
+
+EXE = $(FOAM_APPBIN)/Test-vtkSeriesWriter
diff --git a/applications/test/vtkSeriesWriter/Make/options b/applications/test/vtkSeriesWriter/Make/options
new file mode 100644
index 00000000000..7ce182425d9
--- /dev/null
+++ b/applications/test/vtkSeriesWriter/Make/options
@@ -0,0 +1,5 @@
+EXE_INC = \
+    -I$(LIB_SRC)/fileFormats/lnInclude
+
+EXE_LIBS = \
+    -lfileFormats
diff --git a/applications/test/vtkSeriesWriter/Test-vtkSeriesWriter.C b/applications/test/vtkSeriesWriter/Test-vtkSeriesWriter.C
new file mode 100644
index 00000000000..043e42e0349
--- /dev/null
+++ b/applications/test/vtkSeriesWriter/Test-vtkSeriesWriter.C
@@ -0,0 +1,118 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2018 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+Application
+    Test-vtkSeriesWriter
+
+Description
+    Basic functionality tests for vtk::seriesWriter
+
+\*---------------------------------------------------------------------------*/
+
+#include "foamVtkSeriesWriter.H"
+#include "argList.H"
+
+using namespace Foam;
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+int main(int argc, char *argv[])
+{
+    argList::addBoolOption("sort", "Sort value / name");
+    argList::addBoolOption("check", "Check for existence of files");
+    argList::addOption("time", "value", "Filter based on given time");
+    argList::addOption("scan", "series", "Scan directory to create series");
+
+    argList args(argc, argv, false, true);
+
+    const scalar currTime = args.lookupOrDefault<scalar>("time", GREAT);
+
+    Info<< "Using currTime = " << currTime << nl
+        << "when loading " << (args.size()-1) << " files" << nl << nl;
+
+    for (label argi=1; argi < args.size(); ++argi)
+    {
+        const auto& input = args[argi];
+
+        Info << "load from " << input << nl;
+
+        vtk::seriesWriter writer;
+        writer.load(input);
+
+        writer.print(Info);
+        Info<< nl << nl;
+
+        if (writer.removeNewer(currTime))
+        {
+            Info<< "removed entries with time >= " << currTime << nl;
+            writer.print(Info);
+            Info<< nl << nl;
+        }
+
+        if (args.found("sort"))
+        {
+            writer.sort();
+
+            Info<< "sorted" << nl;
+            writer.print(Info);
+            Info<< nl << nl;
+        }
+
+        if (args.found("check"))
+        {
+            writer.load(input, true);
+
+            Info<< "reload, checking the existance of files" << nl;
+            writer.print(Info);
+            Info<< nl << nl;
+        }
+
+        if (writer.empty())
+        {
+            Info<< "No entries" << nl;
+        }
+        else
+        {
+            Info<< writer.size() << " entries" << nl;
+        }
+    }
+
+    if (args.found("scan"))
+    {
+        vtk::seriesWriter writer;
+
+        writer.scan(args.opt<fileName>("scan"));
+
+        Info<< "scanned for files" << nl;
+        writer.print(Info);
+        Info<< nl << nl;
+    }
+
+
+    Info<< "\nEnd\n" << nl;
+
+    return 0;
+}
+
+
+// ************************************************************************* //
diff --git a/applications/test/vtkSeriesWriter/file.vtm b/applications/test/vtkSeriesWriter/file.vtm
new file mode 100644
index 00000000000..28338dea7ac
--- /dev/null
+++ b/applications/test/vtkSeriesWriter/file.vtm
@@ -0,0 +1,4 @@
+<?xml version='1.0'?>
+<!-- case='abc' time='0' index='0' -->
+<VTKFile type='vtkMultiBlockDataSet' version='1.0' byte_order='LittleEndian' header_type='UInt64'>
+  <vtkMultiBlockDataSet>
diff --git a/applications/test/vtkSeriesWriter/file_00000679.vtm b/applications/test/vtkSeriesWriter/file_00000679.vtm
new file mode 100644
index 00000000000..4b00bcbe8f8
--- /dev/null
+++ b/applications/test/vtkSeriesWriter/file_00000679.vtm
@@ -0,0 +1,4 @@
+<?xml version='1.0'?>
+<!-- case='abc' time='0.00125' index='679' -->
+<VTKFile type='vtkMultiBlockDataSet' version='1.0' byte_order='LittleEndian' header_type='UInt64'>
+  <vtkMultiBlockDataSet>
diff --git a/applications/test/vtkSeriesWriter/file_00001025.vtm b/applications/test/vtkSeriesWriter/file_00001025.vtm
new file mode 100644
index 00000000000..3d4cc1c6df7
--- /dev/null
+++ b/applications/test/vtkSeriesWriter/file_00001025.vtm
@@ -0,0 +1,4 @@
+<?xml version='1.0'?>
+<!-- case='abc' time="0.5250" index='1025' -->
+<VTKFile type='vtkMultiBlockDataSet' version='1.0' byte_order='LittleEndian' header_type='UInt64'>
+  <vtkMultiBlockDataSet>
diff --git a/applications/test/vtkSeriesWriter/file_00001234.vtm b/applications/test/vtkSeriesWriter/file_00001234.vtm
new file mode 100644
index 00000000000..b49c580b1ac
--- /dev/null
+++ b/applications/test/vtkSeriesWriter/file_00001234.vtm
@@ -0,0 +1,4 @@
+<?xml version='1.0'?>
+<!-- note="no time entry" -->
+<VTKFile type='vtkMultiBlockDataSet' version='1.0' byte_order='LittleEndian' header_type='UInt64'>
+  <vtkMultiBlockDataSet>
diff --git a/applications/test/vtkSeriesWriter/file_00005680.vtm b/applications/test/vtkSeriesWriter/file_00005680.vtm
new file mode 100644
index 00000000000..feab3b1cff7
--- /dev/null
+++ b/applications/test/vtkSeriesWriter/file_00005680.vtm
@@ -0,0 +1,4 @@
+<?xml version='1.0'?>
+<!-- case='abc' time= index='1234' note="degenerate time entry" -->
+<VTKFile type='vtkMultiBlockDataSet' version='1.0' byte_order='LittleEndian' header_type='UInt64'>
+  <vtkMultiBlockDataSet>
diff --git a/applications/test/vtkSeriesWriter/file_23.vtm b/applications/test/vtkSeriesWriter/file_23.vtm
new file mode 100644
index 00000000000..43ff669bd21
--- /dev/null
+++ b/applications/test/vtkSeriesWriter/file_23.vtm
@@ -0,0 +1,4 @@
+<?xml version='1.0'?>
+<!-- case='abc' time=0.0145 index='1234' note="unquoted time entry" -->
+<VTKFile type='vtkMultiBlockDataSet' version='1.0' byte_order='LittleEndian' header_type='UInt64'>
+  <vtkMultiBlockDataSet>
diff --git a/applications/test/vtkSeriesWriter/test1.vtm.series b/applications/test/vtkSeriesWriter/test1.vtm.series
new file mode 100644
index 00000000000..a5931a31ac9
--- /dev/null
+++ b/applications/test/vtkSeriesWriter/test1.vtm.series
@@ -0,0 +1,20 @@
+{
+  "file-series-version" : "1.0",
+  "files": [
+    { "name" : "file_00001742.vtm", "time" : 50 },
+    { "name" : "file_00001025.vtm", "time" : 30 },
+    {
+        "time" : 40,
+        "name" : "file_00001380.vtm", ,,,,
+    },
+    { "name" : "file_00000679.vtm", "time" : 20 },
+    { "name" : "file_00002110.vtm", "time" : 60 },
+    { "name" : "file_00001234.vtm", "time" : 60 },
+    { "name" : "file_00001742.vtm", "time" : 150 },
+    { "name" : "file_00001742.vtm", "time" : 5 },
+
+    { "name" : "file_1000.vtm", "badTime" : 60 },
+    { "" : "", "" : 60, "" : false },
+    { "name" : "", "time" : 10 },
+  ]
+}
diff --git a/src/fileFormats/Make/files b/src/fileFormats/Make/files
index 640d98882bf..2ed0cc373ab 100644
--- a/src/fileFormats/Make/files
+++ b/src/fileFormats/Make/files
@@ -17,6 +17,7 @@ stl/STLAsciiParseManual.C
 stl/STLAsciiParseRagel.C
 
 vtk/file/foamVtkFileWriter.C
+vtk/file/foamVtkSeriesWriter.C
 vtk/file/foamVtmWriter.C
 vtk/core/foamVtkCore.C
 vtk/core/foamVtkPTraits.C
diff --git a/src/fileFormats/vtk/file/foamVtkSeriesWriter.C b/src/fileFormats/vtk/file/foamVtkSeriesWriter.C
new file mode 100644
index 00000000000..c7d243f578e
--- /dev/null
+++ b/src/fileFormats/vtk/file/foamVtkSeriesWriter.C
@@ -0,0 +1,753 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2018 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+\*---------------------------------------------------------------------------*/
+
+#include "foamVtkSeriesWriter.H"
+#include "Fstream.H"
+#include "ListOps.H"
+#include "stringOpsSort.H"
+#include "OSspecific.H"
+
+// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+    // Get any single token.
+    static inline bool getToken(ISstream& is, token& tok)
+    {
+        return (!is.read(tok).bad() && tok.good());
+    }
+
+    // Get two tokens.
+    // The first one must be a ':' token, the second one is any value
+    //
+    // This corrsponds to the JSON  "key" : value syntax,
+    // we trigger after reading the "key".
+    static inline bool getValueToken(ISstream& is, token& tok)
+    {
+        return
+        (
+            // Token 1 = ':' separator
+            (
+                getToken(is, tok)
+             && tok.isPunctuation() && tok.pToken() == token::COLON
+            )
+
+            // Token 2 is the value
+         && getToken(is, tok)
+        );
+    }
+
+
+    // Sorting for fileNameInstant
+    //   1. sort by value (time)
+    //   2. natural sort (name)
+    struct seriesLess
+    {
+        bool operator()(const fileNameInstant a, const fileNameInstant b) const
+        {
+            scalar val = compareOp<scalar>()(a.value(), b.value());
+            if (val == 0)
+            {
+                return
+                    stringOps::natural_sort::compare(a.name(), b.name()) < 0;
+            }
+            return val < 0;
+        }
+    };
+
+
+    // Check if value is less than upper, with some tolerance.
+    static inline bool lessThan(const scalar& val, const scalar& upper)
+    {
+        return (val < upper && Foam::mag(val - upper) > ROOTVSMALL);
+    }
+}
+
+
+// * * * * * * * * * * * * * Static Member Functions * * * * * * * * * * * * //
+
+Foam::fileName Foam::vtk::seriesWriter::base
+(
+    const fileName& outputName,
+    char sep
+)
+{
+    const auto dash = outputName.rfind(sep);
+
+    // No separator? Or separator in path() instead of name()?
+    if
+    (
+        std::string::npos == dash
+     || std::string::npos != outputName.find('/', dash)
+    )
+    {
+        // Warn?
+        return outputName;
+    }
+
+    const auto dot = outputName.find('.', dash);
+
+    if (std::string::npos == dot)
+    {
+        return outputName.substr(0, dash);
+    }
+
+    return outputName.substr(0, dash) + outputName.substr(dot);
+}
+
+
+Foam::word Foam::vtk::seriesWriter::suffix
+(
+    const fileName& file,
+    char sep
+)
+{
+    const auto dash = file.rfind(sep);
+
+    // No separator? Or separator in path() instead of name()?
+    if
+    (
+        std::string::npos == dash
+     || std::string::npos != file.find('/', dash)
+    )
+    {
+        // Warn?
+        return "";
+    }
+
+    const auto dot = file.find('.', dash);
+
+    if (std::string::npos == dot)
+    {
+        return file.substr(dash);
+    }
+
+    return file.substr(dash, (dot-dash));
+}
+
+
+Foam::Ostream& Foam::vtk::seriesWriter::print
+(
+    Ostream& os,
+    const fileName& base,
+    const UList<instant>& series,
+    const char sep
+)
+{
+    // Split the base into (stem, ext) components
+    //
+    // base = "path/file.vtm"
+    //
+    // stem = "file"
+    // ext = ".vtm"
+
+    const word stem = base.nameLessExt();
+    const word ext = "." + base.ext();
+
+    // Begin file-series (JSON)
+    os  << "{\n  \"file-series-version\" : \"1.0\",\n  \"files\" : [\n";
+
+    // Track how many entries are remaining
+    // - trailing commas on all but the final entry (JSON requirement)
+    label nremain = series.size();
+
+    // Each entry
+    //   { "name" : "<stem><sep>name<ext>",  "time" : value }
+
+    for (const instant& inst : series)
+    {
+        os  << "    { \"name\" : \""
+            << stem << sep << inst.name() << ext
+            << "\", \"time\" : " << inst.value() << " }";
+
+        if (--nremain)
+        {
+            os  << ',';
+        }
+        os  << nl;
+    }
+
+    os  << "  ]\n}\n";
+
+    return os;
+}
+
+
+Foam::Ostream& Foam::vtk::seriesWriter::print
+(
+    Ostream& os,
+    const UList<fileNameInstant>& series
+)
+{
+    // Begin file-series (JSON)
+    os  << "{\n  \"file-series-version\" : \"1.0\",\n  \"files\" : [\n";
+
+    // Track how many entries are remaining
+    // - trailing commas on all but the final entry (JSON requirement)
+    label nremain = series.size();
+
+    // Each entry
+    //   { "name" : "<file>",  "time" : <value> }
+
+    for (const fileNameInstant& inst : series)
+    {
+        os  << "    { \"name\" : \""
+            << inst.name().name()
+            << "\", \"time\" : " << inst.value() << " }";
+
+        if (--nremain)
+        {
+            os  << ',';
+        }
+        os  << nl;
+    }
+
+    os  << "  ]\n}\n";
+
+    return os;
+}
+
+
+void Foam::vtk::seriesWriter::write
+(
+    const fileName& seriesName,
+    const UList<instant>& series,
+    const char sep
+)
+{
+    mkDir(seriesName.path());
+
+    autoPtr<OFstream> osPtr =
+    (
+        seriesName.hasExt("series")
+      ? autoPtr<OFstream>::New(seriesName)
+      : autoPtr<OFstream>::New(seriesName + ".series")
+    );
+
+    print(*osPtr, seriesName, series, sep);
+}
+
+
+
+void Foam::vtk::seriesWriter::write
+(
+    const fileName& seriesName,
+    const UList<fileNameInstant>& series
+)
+{
+    mkDir(seriesName.path());
+
+    autoPtr<OFstream> osPtr =
+    (
+        seriesName.hasExt("series")
+      ? autoPtr<OFstream>::New(seriesName)
+      : autoPtr<OFstream>::New(seriesName + ".series")
+    );
+
+    print(*osPtr, series);
+}
+
+
+// * * * * * * * * * * * * * Private Member Functions  * * * * * * * * * * * //
+
+bool Foam::vtk::seriesWriter::appendCheck(fileNameInstant inst)
+{
+    if (inst.name().empty())
+    {
+        return false;
+    }
+
+    const auto iter = existing_.find(inst.name());
+
+    if (iter.found())
+    {
+        for (fileNameInstant& dst : entries_)
+        {
+            if (dst.name() == inst.name())
+            {
+                // Replace value
+                dst.value() = inst.value();
+                return true;
+            }
+        }
+    }
+
+    entries_.append(inst);
+    existing_.insert(inst.name());
+
+    return true;
+}
+
+
+bool Foam::vtk::seriesWriter::removeDuplicates()
+{
+    const label nElem = entries_.size();
+
+    HashTable<label, fileName> filesSeen(2*nElem);
+
+    bool changed = false;
+
+    for (label elemi=0; elemi < nElem; ++elemi)
+    {
+        fileNameInstant& inst = entries_[elemi];
+
+        if (inst.name().empty())
+        {
+            changed = true;
+        }
+        else
+        {
+            auto iter = filesSeen.find(inst.name());
+
+            if (iter.found())
+            {
+                // Mark previous location as being superseded
+                entries_[*iter].name().clear();
+                changed = true;
+
+                *iter = elemi;  // The latest with this name
+            }
+            else
+            {
+                filesSeen.insert(inst.name(), elemi);
+            }
+        }
+    }
+
+
+    if (changed)
+    {
+        label dsti = 0;
+        for (label elemi=0; elemi < nElem; ++elemi)
+        {
+            fileNameInstant& src = entries_[elemi];
+
+            if (!src.name().empty())
+            {
+                if (dsti != elemi)
+                {
+                    entries_[dsti] = std::move(src);
+                }
+                ++dsti;
+            }
+        }
+
+        entries_.resize(dsti);
+    }
+
+    return (nElem != entries_.size());
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+Foam::label Foam::vtk::seriesWriter::load
+(
+    const fileName& seriesName,
+    const bool checkFiles,
+    const scalar restartTime
+)
+{
+    clear();
+
+    fileName seriesFile(seriesName);
+    if (!seriesFile.hasExt("series"))
+    {
+        seriesFile.ext("series");
+    }
+
+    if (!isFile(seriesFile))
+    {
+        return size();
+    }
+
+    HashSet<fileName> filesOnDisk;
+
+    if (checkFiles)
+    {
+        filesOnDisk.insert(Foam::readDir(seriesFile.path()));
+    }
+
+
+    // Parse JSON content:
+    //
+    // {
+    //   "file-series-version" : "1.0",
+    //   "files" : [
+    //      { "name" : "abc", "time" : 123 },
+    //      { "name" : "def", "time" : 345 }
+    //    ]
+    // }
+
+    // Parsing states
+    enum parse
+    {
+        NONE,         // Looking for "files"
+        FILES_ARRAY,  // Saw "file" : '['
+        ENTRY,        // Parsing in { "name" : ... }
+        DONE,         // Saw a ']' while in FILES_ARRAY
+        FAIL          // Something bad happened
+    };
+
+    // Track if "file" and "time" keys have been located
+    unsigned instStatus = 0;
+    fileNameInstant inst;
+
+    token tok;
+
+    IFstream is(seriesFile);
+
+    for
+    (
+        parse state = parse::NONE;
+        (state != parse::DONE && state != parse::FAIL)
+     && getToken(is, tok);
+        /*nil*/
+    )
+    {
+        switch (state)
+        {
+            // Still scanning for initial "files" entry
+            case parse::NONE :
+            {
+                if (tok.isString() && tok.stringToken() == "files")
+                {
+                    // Expect "files" : [ ...
+
+                    if
+                    (
+                        getValueToken(is, tok)
+                     && tok.isPunctuation()
+                     && tok.pToken() == token::BEGIN_SQR
+                    )
+                    {
+                        state = parse::FILES_ARRAY;
+                    }
+                    else
+                    {
+                        state = parse::FAIL;
+                    }
+                }
+            }
+            break;
+
+            // Parsing entries within "files" array
+            case parse::FILES_ARRAY :
+            {
+                if (tok.isPunctuation())
+                {
+                    switch (tok.pToken())
+                    {
+                        // ',' - keep going (another entry)
+                        case token::COMMA :
+                            break;
+
+                        // '{' - begin entry
+                        case token::BEGIN_BLOCK :
+                            state = parse::ENTRY;
+                            instStatus = 0;
+                            break;
+
+                        // ']' - done array
+                        case token::END_SQR :
+                            state = parse::DONE;
+                            break;
+
+                        default:
+                            state = parse::FAIL;
+                            break;
+                    }
+                }
+                else
+                {
+                    state = parse::FAIL;
+                }
+            }
+            break;
+
+            // Parsing an individual entry within "files"
+            case parse::ENTRY :
+            {
+                if (tok.isPunctuation())
+                {
+                    switch (tok.pToken())
+                    {
+                        // ',' - keep going (another key/value pair)
+                        case token::COMMA :
+                            break;
+
+                        // '}'
+                        case token::END_BLOCK :
+                        {
+                            // Verify instant was properly parsed and
+                            // is also valid
+                            if
+                            (
+                                instStatus == 0x03
+                             && lessThan(inst.value(), restartTime)
+                             &&
+                                (
+                                    checkFiles
+                                  ? filesOnDisk.found(inst.name())
+                                  : true
+                                )
+                            )
+                            {
+                                appendCheck(inst);
+                            }
+
+                            state = parse::FILES_ARRAY;
+                            instStatus = 0;
+                        }
+                        break;
+
+                        default:
+                            state = parse::FAIL;
+                            break;
+                    }
+                }
+                else if (tok.isString())
+                {
+                    // Expect "key" : value
+
+                    const string key(tok.stringToken());
+
+                    if (getValueToken(is, tok))
+                    {
+                        if ("name" == key)
+                        {
+                            if (tok.isString())
+                            {
+                                inst.name() = tok.stringToken();
+                                instStatus |= 0x01;
+                            }
+                            else
+                            {
+                                state = parse::FAIL;
+                            }
+                        }
+                        else if ("time" == key)
+                        {
+                            if (tok.isNumber())
+                            {
+                                inst.value() = tok.number();
+                                instStatus |= 0x02;
+                            }
+                            else
+                            {
+                                state = parse::FAIL;
+                            }
+                        }
+                    }
+                    else
+                    {
+                        state = parse::FAIL;
+                    }
+                }
+                else
+                {
+                    state = parse::FAIL;
+                }
+            }
+            break;
+
+            default:
+                break;
+        }
+    }
+
+    return size();
+}
+
+
+Foam::label Foam::vtk::seriesWriter::scan
+(
+    const fileName& seriesName,
+    const scalar restartTime
+)
+{
+    clear();
+
+    const fileName path = seriesName.path();
+
+    if (!isDir(path))
+    {
+        return size();
+    }
+
+    fileName seriesFile(seriesName);
+
+    if (seriesName.hasExt("series"))
+    {
+        seriesFile.removeExt();
+    }
+
+    const word stem = seriesFile.nameLessExt();
+    const word ext = seriesFile.ext();
+
+    // Accept "fileN.ext", "fileNN.ext", but reject "file.ext"
+    const auto minLen = stem.length() + ext.length() + 1;
+
+    const auto acceptName =
+        [=](const fileName& file) -> bool
+        {
+            return
+            (
+                minLen < file.length()
+             && file.hasExt(ext) && file.startsWith(stem)
+            );
+        };
+
+
+    fileNameList files = subsetList(Foam::readDir(path), acceptName);
+
+    // Names sorted so warnings appear less random
+    Foam::sort(files, stringOps::natural_sort());
+
+    // Scratch space for reading some of the file
+    std::string header;
+
+    scalar timeValue;
+
+    bool warnings = false;
+
+    for (const fileName& file : files)
+    {
+        std::ifstream is(path/file);
+
+        if (!is)
+        {
+            continue;
+        }
+
+        // Read directly into the string
+        // 1024 (12 lines of 80 chars) is plenty for all comments
+
+        header.resize(1024);
+        is.read(&(header.front()), header.size());
+        header.resize(is.gcount());
+
+        // DebugInfo
+        //     << "got header:\n=====\n" << header << "\n=====\n" << nl;
+
+
+        // Look for time="...", time='...', or even time=... attribute
+
+        auto begAttr = header.find("time=");
+
+        if (string::npos == begAttr)
+        {
+            if (!warnings)
+            {
+                Info<< "No 'time=' comment attribute found:\n(" << nl;
+                warnings = true;
+            }
+            Info<< "    " << file << nl;
+            continue;
+        }
+
+        // Skip past the 'time='
+        begAttr += 5;
+        const char quote = header[begAttr];
+
+        // Info<< "have time=" << int(begAttr) << nl;
+
+        auto endAttr =
+        (
+            (quote == '"' || quote == '\'')
+          ?
+            // Quoted
+            header.find(quote, ++begAttr)
+          :
+            // Unquoted
+            header.find_first_of("\t\n\v\f\r ", begAttr)
+        );
+
+
+        if
+        (
+            string::npos != endAttr && begAttr < endAttr
+         && readScalar
+            (
+                header.substr(begAttr, endAttr-begAttr),
+                timeValue
+            )
+         && lessThan(timeValue, restartTime)
+        )
+        {
+            // Success
+            append(timeValue, file);
+        }
+    }
+
+    if (warnings)
+    {
+        Info<< ")" << nl << nl;
+    }
+
+    // Don't trust the order. Sort by time and name instead.
+    this->sort();
+
+    return size();
+}
+
+
+bool Foam::vtk::seriesWriter::removeNewer(const scalar timeValue)
+{
+    // Rebuild hash as side-effect
+    existing_.clear();
+
+    label dsti = 0;
+
+    const label nElem = entries_.size();
+
+    for (label elemi=0; elemi < nElem; ++elemi)
+    {
+        fileNameInstant& src = entries_[elemi];
+
+        if (!src.name().empty() && lessThan(src.value(), timeValue))
+        {
+            if (dsti != elemi)
+            {
+                entries_[dsti] = std::move(src);
+                existing_.insert(entries_[dsti].name());
+            }
+            ++dsti;
+        }
+    }
+
+    entries_.resize(dsti);
+
+    return (nElem != entries_.size());
+}
+
+
+void Foam::vtk::seriesWriter::sort()
+{
+    Foam::sort(entries_, seriesLess());
+}
+
+
+// ************************************************************************* //
diff --git a/src/fileFormats/vtk/file/foamVtkSeriesWriter.H b/src/fileFormats/vtk/file/foamVtkSeriesWriter.H
new file mode 100644
index 00000000000..a62340f5f22
--- /dev/null
+++ b/src/fileFormats/vtk/file/foamVtkSeriesWriter.H
@@ -0,0 +1,276 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2018 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+Class
+    Foam::vtk::seriesWriter
+
+Description
+    Provides a means of accumulating and generating VTK file series.
+
+    The VTK file series format is a simple JSON format with the following
+    type of content:
+    \verbatim
+    {
+      "file-series-version" : "1.0",
+      "files": [
+        { "name" : "file1.vtk", "time" : 10 },
+        { "name" : "file2.vtk", "time" : 20 },
+        { "name" : "file3.vtk", "time" : 30 },
+      ]
+    }
+    \endverbatim
+
+    The append() operations include various sanity checks.
+    Entries with an empty name are ignored.
+    If an entry with an identical name already exists, its place
+    will be overwritten with the new time value.
+
+SourceFiles
+    foamVtkSeriesWriter.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef foamVtkSeriesWriter_H
+#define foamVtkSeriesWriter_H
+
+#include <fstream>
+#include "foamVtkOutputOptions.H"
+#include "instant.H"
+#include "fileNameInstant.H"
+#include "DynamicList.H"
+#include "HashSet.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace vtk
+{
+
+/*---------------------------------------------------------------------------*\
+                       Class vtk::seriesWriter Declaration
+\*---------------------------------------------------------------------------*/
+
+class seriesWriter
+{
+    // Private Member Data
+
+        //- A list of file/time entries
+        DynamicList<fileNameInstant> entries_;
+
+        //- Hash of existing (known) file names
+        HashSet<fileName> existing_;
+
+        //- Append the specified file/time instant.
+        //  Overwrites existing entry that has the same name,
+        //  does not append empty names.
+        bool appendCheck(fileNameInstant inst);
+
+        //- Remove duplicate filename entries. Keeping the last one seen.
+        bool removeDuplicates();
+
+
+public:
+
+    // Constructors
+
+        //- Construct an empty series
+        seriesWriter() = default;
+
+        //- Copy construct
+        seriesWriter(const seriesWriter&) = default;
+
+        //- Move construct
+        seriesWriter(seriesWriter&&) = default;
+
+        //- Copy assignment
+        seriesWriter& operator=(const seriesWriter&) = default;
+
+        //- Move assignment
+        seriesWriter& operator=(seriesWriter&&) = default;
+
+
+    //- Destructor
+    ~seriesWriter() = default;
+
+
+    // Static Member Functions
+
+        //- Extract the base name for a file series
+        //
+        //  \param outputName   The name of the data output file
+        //      Eg, "somefile_0001.vtk" would extract to "somefile.vtk"
+        //  \param sep    The separator used between file stem and suffix.
+        static fileName base(const fileName& outputName, char sep = '_');
+
+        //- Extract the time-varying ending of files
+        //
+        //  \param file   The name of the file
+        //      Eg, "somefile_0001.vtk" would extract to "0001"
+        //  \param sep    The separator used between file stem and suffix.
+        static word suffix(const fileName& file, char sep = '_');
+
+        //- Print file series (JSON format) for specified time instances
+        //
+        //  \param os     The output stream
+        //  \param base   The name for the series (eg, "path/file.vtk")
+        //  \param series The list of suffix/value entries
+        //  \param sep    The separator used between file stem and suffix.
+        static Ostream& print
+        (
+            Ostream& os,
+            const fileName& seriesName,
+            const UList<instant>& series,
+            const char sep = '_'
+        );
+
+        //- Write file series (JSON format) to disk, for specified instances
+        //
+        //  \param base   The name for the series (eg, "path/file.vtk")
+        //  \param series The list of suffix/value entries
+        //  \param sep    The separator used between file stem and suffix.
+        static void write
+        (
+            const fileName& base,
+            const UList<instant>& series,
+            const char sep = '_'
+        );
+
+        //- Print file series (JSON format) for specified time instances.
+        //  Since the VTK file series does not currently (OCT-2018) support
+        //  sub-directories, these will be stripped on output.
+        //
+        //  \param os     The output stream
+        //  \param series The list of filename/value entries
+        static Ostream& print
+        (
+            Ostream& os,
+            const UList<fileNameInstant>& series
+        );
+
+        //- Write file series (JSON format) to disk, for specified instances
+        //
+        //  \param seriesName  The name for the series (eg, "path/file.vtk")
+        //  \param series The list of filename/value entries
+        static void write
+        (
+            const fileName& seriesName,
+            const UList<fileNameInstant>& series
+        );
+
+
+    // Member Functions
+
+        //- True if there are no data sets
+        inline bool empty() const;
+
+        //- The number of data sets
+        inline label size() const;
+
+
+    // Content Management
+
+        //- Clear entries
+        inline void clear();
+
+        //- Append the specified file instant
+        inline bool append(const fileNameInstant& inst);
+
+        //- Append the specified file instant
+        inline bool append(fileNameInstant&& inst);
+
+        //- Append the specified file instant.
+        inline bool append(scalar timeValue, const fileName& file);
+
+        //- Append the specified file instant.
+        inline bool append(scalar timeValue, fileName&& file);
+
+        //- Clear contents and reload by parsing the specified file.
+        //
+        //  \param seriesName the base name of the series to scan, without
+        //      the ".series" ending.
+        //  \param checkFiles verify that the files also exist
+        //  \param restartTime ignore entries with a time greater/equal
+        //      to the specified restart time.
+        //
+        //  \return the number of entries
+        label load
+        (
+            const fileName& seriesName,
+            const bool checkFiles = false,
+            const scalar restartTime = ROOTVGREAT
+        );
+
+        //- Clear contents and scan directory for files.
+        //
+        //  The expected xml header content is a comment with the following:
+        //  \verbatim
+        //     <!-- ... time='3.14159' ... -->
+        //  \endverbatim
+        //
+        //  \param seriesName the base name of the series to scan, without
+        //      the ".series" ending.
+        //  \param restartTime ignore entries with a time greater/equal
+        //      to the specified restart time.
+        //
+        //  \return the number of entries
+        label scan
+        (
+            const fileName& seriesName,
+            const scalar restartTime = ROOTVGREAT
+        );
+
+        //- Remove entries that are greater_equal the time value.
+        //
+        //  \return True if the contents changed
+        bool removeNewer(const scalar timeValue);
+
+        //- Sort by time value and by file name
+        void sort();
+
+
+    // Writing
+
+        //- Print file series as (JSON format)
+        inline void print(Ostream& os) const;
+
+        //- Write file series as (JSON format) to disk
+        inline void write(const fileName& seriesName) const;
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace vtk
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#include "foamVtkSeriesWriterI.H"
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/src/fileFormats/vtk/file/foamVtkSeriesWriterI.H b/src/fileFormats/vtk/file/foamVtkSeriesWriterI.H
new file mode 100644
index 00000000000..263e860b65c
--- /dev/null
+++ b/src/fileFormats/vtk/file/foamVtkSeriesWriterI.H
@@ -0,0 +1,99 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | Copyright (C) 2018 OpenCFD Ltd.
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+License
+    This file is part of OpenFOAM.
+
+    OpenFOAM is free software: you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
+
+\*---------------------------------------------------------------------------*/
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+inline bool Foam::vtk::seriesWriter::empty() const
+{
+    return entries_.empty();
+}
+
+
+inline Foam::label Foam::vtk::seriesWriter::size() const
+{
+    return entries_.size();
+}
+
+
+inline void Foam::vtk::seriesWriter::clear()
+{
+    entries_.clear();
+    existing_.clear();
+}
+
+
+inline bool Foam::vtk::seriesWriter::append(const fileNameInstant& inst)
+{
+    // Strip out path before saving
+    return appendCheck(fileNameInstant(inst.value(), inst.name().name()));
+}
+
+
+inline bool Foam::vtk::seriesWriter::append(fileNameInstant&& inst)
+{
+    // Strip out path before saving
+    inst.name().removePath();
+
+    return appendCheck(inst);
+}
+
+
+inline bool Foam::vtk::seriesWriter::append
+(
+    scalar timeValue,
+    const fileName& file
+)
+{
+    // Strip out path before saving
+    return appendCheck(fileNameInstant(timeValue, file.name()));
+}
+
+
+inline bool Foam::vtk::seriesWriter::append
+(
+    scalar timeValue,
+    fileName&& file
+)
+{
+    // Strip out path before saving
+    file.removePath();
+
+    return appendCheck(fileNameInstant(timeValue, std::move(file)));
+}
+
+
+inline void Foam::vtk::seriesWriter::print(Ostream& os) const
+{
+    seriesWriter::print(os, entries_);
+}
+
+
+inline void Foam::vtk::seriesWriter::write(const fileName& seriesName) const
+{
+    seriesWriter::write(seriesName, entries_);
+}
+
+
+// ************************************************************************* //
-- 
GitLab