diff --git a/app/otbPatchesExtraction.cxx b/app/otbPatchesExtraction.cxx
index a3da71f3563cb176a146af6f4e6dd8f3d86cf171..b00c3a1706ae32973c3f98e96b3acb5b4621939c 100644
--- a/app/otbPatchesExtraction.cxx
+++ b/app/otbPatchesExtraction.cxx
@@ -102,7 +102,7 @@ public:
     AddParameter(ParameterType_Int,            ss_key_dims_y.str(), ss_desc_dims_y.str());
     SetMinimumParameterIntValue               (ss_key_dims_y.str(), 1);
     AddParameter(ParameterType_Float,          ss_key_nodata.str(), ss_desc_nodata.str());
-    SetDefaultParameterFloat                  (ss_key_nodata.str(), 0);
+    MandatoryOff                              (ss_key_nodata.str());
 
     // Add a new bundle
     SourceBundle bundle;
@@ -132,7 +132,11 @@ public:
       bundle.m_PatchSize[1] = GetParameterInt(bundle.m_KeyPszY);
 
       // No data value
-      bundle.m_NoDataValue = GetParameterFloat(bundle.m_KeyNoData);
+      if (HasValue(bundle.m_KeyNoData))
+	{
+        bundle.m_NoDataValue = GetParameterFloat(bundle.m_KeyNoData);
+        }
+
     }
   }
 
@@ -169,10 +173,6 @@ public:
     // Input vector data
     AddParameter(ParameterType_InputVectorData, "vec", "Positions of the samples (must be in the same projection as input image)");
 
-    // No data parameters
-    AddParameter(ParameterType_Bool, "usenodata", "Reject samples that have no-data value");
-    MandatoryOff                    ("usenodata");
-
     // Output label
     AddParameter(ParameterType_OutputImage, "outlabels", "output labels");
     SetDefaultOutputPixelType              ("outlabels", ImagePixelType_uint8);
@@ -201,14 +201,18 @@ public:
     SamplerType::Pointer sampler = SamplerType::New();
     sampler->SetInputVectorData(GetParameterVectorData("vec"));
     sampler->SetField(GetParameterAsString("field"));
-    if (GetParameterInt("usenodata")==1)
-      {
-      otbAppLogINFO("Rejecting samples that have at least one no-data value");
-      sampler->SetRejectPatchesWithNodata(true);
-      }
+
     for (auto& bundle: m_Bundles)
     {
-      sampler->PushBackInputWithPatchSize(bundle.m_ImageSource.Get(), bundle.m_PatchSize, bundle.m_NoDataValue);
+      if (HasValue(bundle.m_KeyNoData)) 
+        {
+        otbAppLogINFO("Rejecting samples that have at least one no-data value");
+        sampler->PushBackInputWithPatchSize(bundle.m_ImageSource.Get(), bundle.m_PatchSize, bundle.m_NoDataValue);
+        }
+      else
+        {
+        sampler->PushBackInputWithPatchSize(bundle.m_ImageSource.Get(), bundle.m_PatchSize);
+        }
     }
 
     // Run the filter
diff --git a/app/otbPatchesSelection.cxx b/app/otbPatchesSelection.cxx
index 5d8165a019764a50a5f3301877cb364108c1f92c..68d76221dbf6b7ffbb828a751420a854d58864a7 100644
--- a/app/otbPatchesSelection.cxx
+++ b/app/otbPatchesSelection.cxx
@@ -1,7 +1,7 @@
 /*=========================================================================
 
      Copyright (c) 2018-2019 IRSTEA
-     Copyright (c) 2020-2021 INRAE
+     Copyright (c) 2020-2022 INRAE
 
 
      This software is distributed WITHOUT ANY WARRANTY; without even
@@ -26,11 +26,15 @@
 #include "itkNearestNeighborInterpolateImageFunction.h"
 #include "itkMaskImageFilter.h"
 
-// image utils
+// Image utils
 #include "otbTensorflowCommon.h"
 #include "otbTensorflowSamplingUtils.h"
 #include "itkImageRegionConstIteratorWithOnlyIndex.h"
 
+// Math
+#include <random>
+#include <limits>
+
 // Functor to retrieve nodata
 template<class TPixel, class OutputPixel>
 class IsNoData
@@ -119,10 +123,7 @@ public:
 
     // Input no-data value
     AddParameter(ParameterType_Float, "nodata", "nodata value");
-    MandatoryOn                      ("nodata");
-    SetDefaultParameterFloat         ("nodata", 0);
-    AddParameter(ParameterType_Bool,  "nocheck", "If on, no check on the validity of patches is performed");
-    MandatoryOff                     ("nocheck");
+    MandatoryOff                     ("nodata");
 
     // Grid
     AddParameter(ParameterType_Group, "grid", "grid settings");
@@ -137,7 +138,27 @@ public:
 
     // Strategy
     AddParameter(ParameterType_Choice, "strategy", "Selection strategy for validation/training patches");
-    AddChoice("strategy.chessboard", "fifty fifty, like a chess board");
+    // Chess board
+    AddChoice("strategy.chessboard", "Fifty fifty with chess-board-like layout. Only \"outtrain\" and "
+        "\"outvalid\" output parameters are used.");
+    // Split
+    AddChoice("strategy.split", "The traditional training/validation/test split. The \"outtrain\", "
+        "\"outvalid\" and \"outtest\" output parameters are used.");
+    AddParameter(ParameterType_Bool, "strategy.split.random", "If false, samples will always be from "
+        "the same group");
+    MandatoryOff                     ("strategy.split.random");
+    AddParameter(ParameterType_Float, "strategy.split.trainprop", "Proportion of training population.");
+    SetMinimumParameterFloatValue    ("strategy.split.trainprop", 0.0);
+    SetDefaultParameterFloat         ("strategy.split.trainprop", 50.0);
+    AddParameter(ParameterType_Float, "strategy.split.validprop", "Proportion of validation population.");
+    SetMinimumParameterFloatValue    ("strategy.split.validprop", 0.0);
+    SetDefaultParameterFloat         ("strategy.split.validprop", 25.0);
+    AddParameter(ParameterType_Float, "strategy.split.testprop", "Proportion of test population.");
+    SetMinimumParameterFloatValue    ("strategy.split.testprop", 0.0);
+    SetDefaultParameterFloat         ("strategy.split.testprop", 25.0);
+    // All
+    AddChoice("strategy.all", "All locations. Only the \"outtrain\" output parameter is used.");
+    // Balanced (experimental)
     AddChoice("strategy.balanced", "you can chose the degree of spatial randomness vs class balance");
     AddParameter(ParameterType_Float, "strategy.balanced.sp", "Spatial proportion: between 0 and 1, "
         "indicating the amount of randomly sampled data in space");
@@ -153,6 +174,9 @@ public:
     // Output points
     AddParameter(ParameterType_OutputVectorData, "outtrain", "output set of points (training)");
     AddParameter(ParameterType_OutputVectorData, "outvalid", "output set of points (validation)");
+    MandatoryOff("outvalid");
+    AddParameter(ParameterType_OutputVectorData, "outtest", "output set of points (test)");
+    MandatoryOff("outtest");
 
     AddRAMParameter();
 
@@ -162,14 +186,14 @@ public:
   {
   public:
     SampleBundle(){}
-    explicit SampleBundle(unsigned int nClasses): dist(DistributionType(nClasses)), id(0), black(true){
+    explicit SampleBundle(unsigned int nClasses): dist(DistributionType(nClasses)), id(0), group(true){
       (void) point;
       (void) index;
     }
     ~SampleBundle(){}
 
     SampleBundle(const SampleBundle & other): dist(other.GetDistribution()), id(other.GetSampleID()),
-      point(other.GetPosition()), black(other.GetBlack()), index(other.GetIndex())
+      point(other.GetPosition()), group(other.GetGroup()), index(other.GetIndex())
     {}
 
     DistributionType GetDistribution() const
@@ -202,14 +226,14 @@ public:
       return point;
     }
 
-    bool& GetModifiableBlack()
+    int& GetModifiableGroup()
     {
-      return black;
+      return group;
     }
 
-    bool GetBlack() const
+    int GetGroup() const
     {
-      return black;
+      return group;
     }
 
     UInt8ImageType::IndexType& GetModifiableIndex()
@@ -227,7 +251,7 @@ public:
     DistributionType dist;
     unsigned int id;
     DataNodePointType point;
-    bool black;
+    int group;
     UInt8ImageType::IndexType index;
   };
 
@@ -252,9 +276,9 @@ public:
 
     UInt8ImageType::Pointer inputImage;
     bool readInput = true;
-    if (GetParameterInt("nocheck")==1)
+    if (!HasValue("nodata"))
       {
-      otbAppLogINFO("\"nocheck\" mode is enabled. Input image pixels no-data values will not be checked.");
+      otbAppLogINFO("No value specified for no-data. Input image pixels no-data values will not be checked.");
       if (HasValue("mask"))
         {
         otbAppLogINFO("Using the provided \"mask\" parameter.");
@@ -362,18 +386,19 @@ public:
       const UInt8ImageType::IndexType & pos, const UInt8ImageType::PointType & geo)
   {
     // Black or white
-    bool black = ((pos[0] + pos[1]) % 2 == 0);
+    int black = (pos[0] + pos[1]) % 2;
 
     bundle.GetModifiableSampleID() = count;
     bundle.GetModifiablePosition() = geo;
-    bundle.GetModifiableBlack() = black;
+    bundle.GetModifiableGroup() = black;
     bundle.GetModifiableIndex() = pos;
     count++;
 
   }
 
   /*
-   * Samples are placed at regular intervals
+   * Samples are placed at regular intervals with the same layout as a chessboard,
+   * in two groups (A: black, B: white)
    */
   void SampleChessboard()
   {
@@ -393,6 +418,75 @@ public:
     PopulateVectorData(bundles);
   }
 
+  void SetSplitBundle(SampleBundle & bundle, unsigned int & count,
+      const UInt8ImageType::IndexType & pos, const UInt8ImageType::PointType & geo,
+      const std::vector<int> & groups)
+  {
+
+    bundle.GetModifiableGroup() = groups[count];
+    bundle.GetModifiableSampleID() = count;
+    bundle.GetModifiablePosition() = geo;
+    bundle.GetModifiableIndex() = pos;
+    count++;
+  }
+
+  /*
+   * Samples are split in training/validation/test groups
+   */
+  void SampleSplit(float trp, float vp, float tp)
+  {
+
+    std::vector<SampleBundle> bundles = AllocateSamples();
+
+    // Populate groups
+    unsigned int nbSamples = bundles.size();
+    float tot = (trp + vp + tp);
+    std::vector<float> props = {trp, vp, tp};
+    std::vector<float> incs, counts;
+    for (auto& prop: props)
+    {
+      if (prop > 0)
+      {
+        incs.push_back(tot / prop);
+        counts.push_back(.0);
+      }
+      else
+        {
+        incs.push_back(.0);
+        counts.push_back((float) nbSamples);
+        }
+    }
+    std::vector<int> groups;
+    for (unsigned int i = 0; i < nbSamples; i++)
+    {
+      // Find the group with the less samples
+      auto it = std::min_element(std::begin(counts), std::end(counts));
+      auto idx = std::distance(std::begin(counts), it);
+      assert (idx > 0);
+      // Assign the group number, and update counts
+      groups.push_back(idx);
+      counts[idx] += incs[idx];
+    }
+    if (GetParameterInt("strategy.split.random") > 0)
+    {
+      // Shuffle groups
+      auto rng = std::default_random_engine {};
+      std::shuffle(std::begin(groups), std::end(groups), rng);
+    }
+
+    unsigned int count = 0;
+    auto lambda = [this, &count, &bundles, &groups]
+                   (const UInt8ImageType::IndexType & pos, const UInt8ImageType::PointType & geo) {
+      SetSplitBundle(bundles[count], count, pos, geo, groups);
+    };
+
+    Apply(lambda);
+    bundles.resize(count);
+
+    // Export training/validation samples
+    PopulateVectorData(bundles);
+  }
+
   void SampleBalanced()
   {
 
@@ -540,14 +634,19 @@ public:
     // Get data tree
     DataTreeType::Pointer treeTrain = m_OutVectorDataTrain->GetDataTree();
     DataTreeType::Pointer treeValid = m_OutVectorDataValid->GetDataTree();
+    DataTreeType::Pointer treeTest = m_OutVectorDataTest->GetDataTree();
     DataNodePointer rootTrain = treeTrain->GetRoot()->Get();
     DataNodePointer rootValid = treeValid->GetRoot()->Get();
+    DataNodePointer rootTest = treeTest->GetRoot()->Get();
     DataNodePointer documentTrain = DataNodeType::New();
     DataNodePointer documentValid = DataNodeType::New();
+    DataNodePointer documentTest = DataNodeType::New();
     documentTrain->SetNodeType(DOCUMENT);
     documentValid->SetNodeType(DOCUMENT);
+    documentTest->SetNodeType(DOCUMENT);
     treeTrain->Add(documentTrain, rootTrain);
     treeValid->Add(documentValid, rootValid);
+    treeTest->Add(documentTest, rootTest);
 
     unsigned int id = 0;
     for (const auto& sample: samples)
@@ -559,16 +658,21 @@ public:
       id++;
 
       // select this sample
-      if (sample.GetBlack())
+      if (sample.GetGroup() == 0)
       {
         // Train
         treeTrain->Add(newDataNode, documentTrain);
       }
-      else
+      else if (sample.GetGroup() == 1)
       {
         // Valid
         treeValid->Add(newDataNode, documentValid);
       }
+      else if (sample.GetGroup() == 2)
+      {
+        // Test
+        treeTest->Add(newDataNode, documentTest);
+      }
 
     }
   }
@@ -580,7 +684,12 @@ public:
 
     // Compute no-data mask
     m_NoDataFilter = IsNoDataFilterType::New();
-    m_NoDataFilter->GetFunctor().SetNoDataValue(GetParameterFloat("nodata"));
+    float nodataValue = std::numeric_limits<float>::quiet_NaN();
+    if (HasValue("nodata"))
+    {
+      nodataValue = GetParameterFloat("nodata");
+    }
+    m_NoDataFilter->GetFunctor().SetNoDataValue(nodataValue);
     m_NoDataFilter->SetInput(GetParameterFloatVectorImage("in"));
     m_NoDataFilter->UpdateOutputInformation();
     UInt8ImageType::Pointer src = m_NoDataFilter->GetOutput();
@@ -630,14 +739,21 @@ public:
     // Prepare output vector data
     m_OutVectorDataTrain = VectorDataType::New();
     m_OutVectorDataValid = VectorDataType::New();
+    m_OutVectorDataTest = VectorDataType::New();
     m_OutVectorDataTrain->SetProjectionRef(m_MorphoFilter->GetOutput()->GetProjectionRef());
     m_OutVectorDataValid->SetProjectionRef(m_MorphoFilter->GetOutput()->GetProjectionRef());
+    m_OutVectorDataTest->SetProjectionRef(m_MorphoFilter->GetOutput()->GetProjectionRef());
 
     if (GetParameterAsString("strategy") == "chessboard")
     {
       otbAppLogINFO("Sampling at regular interval in space (\"Chessboard\" like)");
 
       SampleChessboard();
+
+      if (HasValue("outtest"))
+      {
+        otbAppLogWARNING("The \"outtest\" parameter is unused with the \"chessboard\" sampling strategy.")
+      }
     }
     else if (GetParameterAsString("strategy") == "balanced")
     {
@@ -645,11 +761,45 @@ public:
 
       SampleBalanced();
     }
+    else if (GetParameterAsString("strategy") == "split")
+    {
+      otbAppLogINFO("Sampling with split strategy (Train/Validation/test)");
+      float vp = .0;
+      float tp = .0;
+      if (HasValue("outvalid"))
+      {
+        vp = GetParameterFloat("strategy.split.validprop");
+      }
+      if (HasValue("outtest"))
+      {
+        tp = GetParameterFloat("strategy.split.testprop");
+      }
+
+      SampleSplit(GetParameterFloat("strategy.split.trainprop"), vp, tp);
+    }
+    else if (GetParameterAsString("strategy") == "all")
+    {
+      otbAppLogINFO("Sampling all locations (only \"outtrain\" output parameter will be used");
+
+      SampleSplit(1.0, .0, .0);
+
+      if (HasValue("outtest") || HasValue("outvalid"))
+      {
+        otbAppLogWARNING("The \"outvalid\" and \"outtest\" parameters are unused with the \"all\" sampling strategy.")
+      }
+    }
 
     otbAppLogINFO( "Writing output samples positions");
 
     SetParameterOutputVectorData("outtrain", m_OutVectorDataTrain);
-    SetParameterOutputVectorData("outvalid", m_OutVectorDataValid);
+    if (HasValue("outvalid") && GetParameterAsString("strategy") != "all")
+    {
+      SetParameterOutputVectorData("outvalid", m_OutVectorDataValid);
+    }
+    if (HasValue("outtest") && GetParameterAsString("strategy") == "split")
+    {
+      SetParameterOutputVectorData("outtest", m_OutVectorDataTest);
+    }
 
   }
 
@@ -665,6 +815,7 @@ private:
   MorphoFilterType::Pointer    m_MorphoFilter;
   VectorDataType::Pointer      m_OutVectorDataTrain;
   VectorDataType::Pointer      m_OutVectorDataValid;
+  VectorDataType::Pointer      m_OutVectorDataTest;
   MaskImageFilterType::Pointer m_MaskImageFilter;
 }; // end of class
 
diff --git a/include/otbTensorflowSampler.h b/include/otbTensorflowSampler.h
index 4fae38e75245ca417c638105379ec7ff7dddf6dd..0252db09565f038468340e7c47b0342dfa4f4685 100644
--- a/include/otbTensorflowSampler.h
+++ b/include/otbTensorflowSampler.h
@@ -102,13 +102,11 @@ public:
   /** Set / get image */
   virtual void
   PushBackInputWithPatchSize(const ImageType * input, SizeType & patchSize, InternalPixelType nodataval);
+  virtual void
+  PushBackInputWithPatchSize(const ImageType * input, SizeType & patchSize);
   const ImageType *
   GetInput(unsigned int index);
 
-  /** Set / get no-data related parameters */
-  itkSetMacro(RejectPatchesWithNodata, bool);
-  itkGetMacro(RejectPatchesWithNodata, bool);
-
   /** Do the real work */
   virtual void
   Update();
@@ -144,8 +142,7 @@ private:
   unsigned long        m_NumberOfRejectedSamples;
 
   // No data stuff
-  std::vector<InternalPixelType> m_NoDataValues;
-  bool                           m_RejectPatchesWithNodata;
+  std::map<unsigned int, InternalPixelType> m_NoDataValues;
 
 }; // end class
 
diff --git a/include/otbTensorflowSampler.hxx b/include/otbTensorflowSampler.hxx
index 77558c7ba08c6dc75ce8ced1d389a537150a68f7..966a37969c43ffdb9b7df32ea21dc8a0c7330dd2 100644
--- a/include/otbTensorflowSampler.hxx
+++ b/include/otbTensorflowSampler.hxx
@@ -22,7 +22,6 @@ TensorflowSampler<TInputImage, TVectorData>::TensorflowSampler()
 {
   m_NumberOfAcceptedSamples = 0;
   m_NumberOfRejectedSamples = 0;
-  m_RejectPatchesWithNodata = false;
 }
 
 template <class TInputImage, class TVectorData>
@@ -33,7 +32,17 @@ TensorflowSampler<TInputImage, TVectorData>::PushBackInputWithPatchSize(const Im
 {
   this->ProcessObject::PushBackInput(const_cast<ImageType *>(input));
   m_PatchSizes.push_back(patchSize);
-  m_NoDataValues.push_back(nodataval);
+  unsigned int index = m_PatchSizes.size() -1 ;
+  m_NoDataValues[index] = nodataval;
+}
+
+template <class TInputImage, class TVectorData>
+void
+TensorflowSampler<TInputImage, TVectorData>::PushBackInputWithPatchSize(const ImageType * input,
+                                                                        SizeType &        patchSize)
+{
+  this->ProcessObject::PushBackInput(const_cast<ImageType *>(input));
+  m_PatchSizes.push_back(patchSize);
 }
 
 template <class TInputImage, class TVectorData>
@@ -187,8 +196,8 @@ TensorflowSampler<TInputImage, TVectorData>::Update()
           // If not, reject this sample
           hasBeenSampled = false;
         }
-        // Check if the sampled patch contains a no-data value
-        if (m_RejectPatchesWithNodata && hasBeenSampled)
+        // If NoData is provided, check if the sampled patch contains a no-data value
+        if (m_NoDataValues.count(i) > 0 && hasBeenSampled)
         {
           IndexType outIndex;
           outIndex[0] = 0;
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index a4260dfe9103806d0ca716022da525da0ee06fca..1b4066913fa6d87382c52f9572fa215e9519f7bb 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -59,6 +59,10 @@ set(PATCHESPOS_01 ${TEMP}/out_train_32.gpkg)
 set(PATCHESPOS_02 ${TEMP}/out_valid_32.gpkg)
 set(PATCHESPOS_11 ${TEMP}/out_train_33.gpkg)
 set(PATCHESPOS_12 ${TEMP}/out_valid_33.gpkg)
+set(PATCHESPOS_SPLIT1 vector_train.geojson)
+set(PATCHESPOS_SPLIT2 vector_valid.geojson)
+set(PATCHESPOS_SPLIT3 vector_test.geojson)
+set(PATCHESPOS_ALL vector_all.geojson)
 # Even patches
 otb_test_application(NAME PatchesSelectionEven
   APP  PatchesSelection
@@ -81,6 +85,43 @@ otb_test_application(NAME PatchesSelectionOdd
   -outvalid ${PATCHESPOS_12}
   )
 
+# Split strategy
+otb_test_application(NAME PatchesSelectionSplit
+  APP  PatchesSelection
+  OPTIONS 
+  -in ${IMAGEPXS}
+  -grid.step 32
+  -grid.psize 32
+  -strategy split
+  -outtrain ${TEMP}/${PATCHESPOS_SPLIT1}
+  -outvalid ${TEMP}/${PATCHESPOS_SPLIT2}
+  -outtest ${TEMP}/${PATCHESPOS_SPLIT3}
+  VALID --compare-ascii ${EPSILON_6}
+  ${DATADIR}/${PATCHESPOS_SPLIT1}
+  ${TEMP}/${PATCHESPOS_SPLIT1}
+  VALID --compare-ascii ${EPSILON_6}
+  ${DATADIR}/${PATCHESPOS_SPLIT2}
+  ${TEMP}/${PATCHESPOS_SPLIT2}
+  VALID --compare-ascii ${EPSILON_6}
+  ${DATADIR}/${PATCHESPOS_SPLIT3}
+  ${TEMP}/${PATCHESPOS_SPLIT3}
+  
+  )
+
+# All strategy
+otb_test_application(NAME PatchesSelectionAll
+  APP  PatchesSelection
+  OPTIONS 
+  -in ${IMAGEPXS}
+  -grid.step 32
+  -grid.psize 32
+  -strategy all
+  -outtrain ${TEMP}/${PATCHESPOS_ALL}
+  VALID --compare-ascii ${EPSILON_6}
+  ${DATADIR}/${PATCHESPOS_ALL}
+  ${TEMP}/${PATCHESPOS_ALL}
+  )
+
 #----------- Patches extraction ----------------
 # Even patches
 otb_test_application(NAME PatchesExtractionEven
diff --git a/test/data/vector_all.geojson b/test/data/vector_all.geojson
new file mode 100644
index 0000000000000000000000000000000000000000..44116919b0893db7e847ac32b43aadb9f5f65e85
--- /dev/null
+++ b/test/data/vector_all.geojson
@@ -0,0 +1,15 @@
+{
+"type": "FeatureCollection",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::2154" } },
+"features": [
+{ "type": "Feature", "properties": { "id": 0 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444396.0 ] } },
+{ "type": "Feature", "properties": { "id": 1 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444396.0 ] } },
+{ "type": "Feature", "properties": { "id": 2 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444396.0 ] } },
+{ "type": "Feature", "properties": { "id": 3 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444348.0 ] } },
+{ "type": "Feature", "properties": { "id": 4 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444348.0 ] } },
+{ "type": "Feature", "properties": { "id": 5 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444348.0 ] } },
+{ "type": "Feature", "properties": { "id": 6 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444300.0 ] } },
+{ "type": "Feature", "properties": { "id": 7 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444300.0 ] } },
+{ "type": "Feature", "properties": { "id": 8 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444300.0 ] } }
+]
+}
diff --git a/test/data/vector_test.geojson b/test/data/vector_test.geojson
new file mode 100644
index 0000000000000000000000000000000000000000..0fbd2f0256b879c2e5347282f755790ec845e522
--- /dev/null
+++ b/test/data/vector_test.geojson
@@ -0,0 +1,8 @@
+{
+"type": "FeatureCollection",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::2154" } },
+"features": [
+{ "type": "Feature", "properties": { "id": 2 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444396.0 ] } },
+{ "type": "Feature", "properties": { "id": 6 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444300.0 ] } }
+]
+}
diff --git a/test/data/vector_train.geojson b/test/data/vector_train.geojson
new file mode 100644
index 0000000000000000000000000000000000000000..e536388981212ac30ca0d1198e9453afda36cf89
--- /dev/null
+++ b/test/data/vector_train.geojson
@@ -0,0 +1,11 @@
+{
+"type": "FeatureCollection",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::2154" } },
+"features": [
+{ "type": "Feature", "properties": { "id": 0 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444396.0 ] } },
+{ "type": "Feature", "properties": { "id": 3 }, "geometry": { "type": "Point", "coordinates": [ 493965.0, 6444348.0 ] } },
+{ "type": "Feature", "properties": { "id": 4 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444348.0 ] } },
+{ "type": "Feature", "properties": { "id": 7 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444300.0 ] } },
+{ "type": "Feature", "properties": { "id": 8 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444300.0 ] } }
+]
+}
diff --git a/test/data/vector_valid.geojson b/test/data/vector_valid.geojson
new file mode 100644
index 0000000000000000000000000000000000000000..3d64b131409939cd43a24ba2b7655f89c2c55b64
--- /dev/null
+++ b/test/data/vector_valid.geojson
@@ -0,0 +1,8 @@
+{
+"type": "FeatureCollection",
+"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::2154" } },
+"features": [
+{ "type": "Feature", "properties": { "id": 1 }, "geometry": { "type": "Point", "coordinates": [ 494013.0, 6444396.0 ] } },
+{ "type": "Feature", "properties": { "id": 5 }, "geometry": { "type": "Point", "coordinates": [ 494061.0, 6444348.0 ] } }
+]
+}