Export/download non-spatial tables to runtime geodatabase

769
6
Jump to solution
09-20-2017 05:00 AM
MarcWouters
New Contributor III

I have published a feature service which has both spatial layers and non-spatial tables.

Next, I want to generate a runtime .geodatabase using GenerateGeodatabaseJob::generateGeodatabase(parameters, fileLocation).

The parameters are generated with GeodatabaseSyncTask::createDefaultGenerateGeodatabaseParameters().

The documentation of this method says 

  • All layers from the service will be included

But also 

  • Related tables and layers are not included.

I guess this included non-spatial tables.

To fix this, I set the GenerateLayerOption.setUseGeometry to False, and add a whereClause to select everything ("1=1").

But still, I get the following error :

Unable to create replica. Please check your parameters." : "Exporting data for layer 10 failed."

First of all, is it possible to download non-spatial tables with these methods ?

And if yes, which settings do I need to change ?

All suggestions are welcome.

PS : There is an "ugly" workaround, but I like to avoid it : convert all non-spatial tables to feature tables, by adding a shape field with 1 point

0 Kudos
1 Solution

Accepted Solutions
LucasDanzinger
Esri Frequent Contributor

We have a test that does this. I just ran it, and it successfully generated the gdb with the table. Here is the code:

// Create default generate geodatabase parameters
auto geodatabaseSyncTask = std::unique_ptr<GeodatabaseSyncTask>(new GeodatabaseSyncTask(url_standAloneTable, this));
VERIFY2(TestObject::waitForLoadableInit(geodatabaseSyncTask.get()), "Sync task did not initialize, tests will fail");

Envelope env(-1.36572476878E7, -1.15446130221E7, 4281966740, 1.41330105216E7, SpatialReference(102100));

// A GenerateLayerOption must be specified, otherwise generateGeodatabaseAsync() throws an exception
GenerateLayerOption option;
option.setLayerId(1);
option.setWhereClause("1=1");

GenerateGeodatabaseParameters params;
params.setLayerOptions(QList<GenerateLayerOption>() << option);

// An extent must be specified, because "Geometry cannot be null in generate geodatabase parameters"
params.setExtent(env);

QString path = QDir::tempPath() + QString("/nonSpatial.geodatabase");
QPointer<GenerateGeodatabaseJob> generateGeodatabaseJob = geodatabaseSyncTask->generateGeodatabase(params, path);

View solution in original post

6 Replies
LucasDanzinger
Esri Frequent Contributor

Marc-

This should be possible without the workaround you suggest. See the suggestion here - GenerateGeodatabaseParameters Class | ArcGIS for Developers 

"For non-spatial records, set GenerateLayerOption::useGeometry to false and provide a GenerateLayerOption::whereClause."

0 Kudos
MarcWouters
New Contributor III

Hi Luke,

I've tried that already, without success. Perhaps my whereClause is not valid ?

(but I've successfully used this in other queries).

params.setSyncModel(SyncModel::Layer);

QList<GenerateLayerOption> glo = params.layerOptions();
for (int i = 0; i < glo.size(); ++i) {
   glo.setQueryOption(GenerateLayerQueryOption::All);
   if (i > 10) {      // the first layers do have geometry
      glo.setUseGeometry(false);
      glo.setWhereClause("1=1");

   }
}

Do I need to change something else ?

0 Kudos
LucasDanzinger
Esri Frequent Contributor

We have a test that does this. I just ran it, and it successfully generated the gdb with the table. Here is the code:

// Create default generate geodatabase parameters
auto geodatabaseSyncTask = std::unique_ptr<GeodatabaseSyncTask>(new GeodatabaseSyncTask(url_standAloneTable, this));
VERIFY2(TestObject::waitForLoadableInit(geodatabaseSyncTask.get()), "Sync task did not initialize, tests will fail");

Envelope env(-1.36572476878E7, -1.15446130221E7, 4281966740, 1.41330105216E7, SpatialReference(102100));

// A GenerateLayerOption must be specified, otherwise generateGeodatabaseAsync() throws an exception
GenerateLayerOption option;
option.setLayerId(1);
option.setWhereClause("1=1");

GenerateGeodatabaseParameters params;
params.setLayerOptions(QList<GenerateLayerOption>() << option);

// An extent must be specified, because "Geometry cannot be null in generate geodatabase parameters"
params.setExtent(env);

QString path = QDir::tempPath() + QString("/nonSpatial.geodatabase");
QPointer<GenerateGeodatabaseJob> generateGeodatabaseJob = geodatabaseSyncTask->generateGeodatabase(params, path);
MarcWouters
New Contributor III

Hi Luke,

I am stupid.

Of course it works, because you actually set the LayerOptions in the params (line 13 in your code snippet)

In my code, I only changed a copy, but didn't assign them back to the actual parameters.

Now it works as documented.

Just one small thing : is there a way to identify the layers in GenerateLayerOption list by name or so?

From the GeodatabaseSyncTask::featureServiceInfo(), I can get the layernames (spatial) and the tablenames (non-spatial). This allows me to calculate the number of the layer in the GenerateLayerOption list with the 1st non-spatial table. There should be an easier way to find this.

0 Kudos
LucasDanzinger
Esri Frequent Contributor

Marc- good catch. 

As for your follow up question, do you have a proposal or some pseudocode of what you would like to do? From what I understand, you are able to get the IDs of the layers and tables in the service, so you should be able to loop through each of these and create appropriate layer options for each list. Let me know what you're thinking.

0 Kudos
MarcWouters
New Contributor III

Luke,

(First of all : how do you add a code snippet with line numbers, like you did above ? Can't find it)

I've modified a code sample I've found somewhere on one of the many ArcGIS webpages 

From the featureServiceInfo(), I can obtain layerInfos() and tableInfos(), which have ID numbers.

Here I can see that lower ID numbers are assigned to the spatial layers, followed by the non-spatial tables.

I cannot assign layerOptions here yet, because my parameters are not generated yet.

I cannot add an extra parameter in the generate(params) method, because of the lambda syntax (I'm not so familiar with this construct, but it works) => global parameter m_numLayersInService.

Like this it works, although I will not win any beauty contest with this 

Marc

void myClass::downloadCopy()  {
   GeodatabaseSyncTask* task = new GeodatabaseSyncTask(QUrl("url_of_service"));

   connect(task, &GeodatabaseSyncTask::doneLoading, this, [this](Error error) {
      if (!error.isEmpty()) {

         qDebug() << "Error loading GeodatabaseSyncTask:" << error.message() 
      }
      else {
         ArcGISFeatureServiceInfo featureServiceInfo = task->featureServiceInfo();
         QList<IdInfo> layerInfo = featureServiceInfo.layerInfos();
         m_numLayersInService = layerInfo.size();

         QList<IdInfo> tableInfo = featureServiceInfo.tableInfos();
         for (int i = 0; i < layerInfo.size(); ++i) {
            qDebug() << "Layer" << layerInfo.name() << "ID" << layerInfo.infoId();
         }
         for (int i = 0; i < tabelInfo.size(); ++i) {
            qDebug() << "Table" << tableInfo.name() << "ID" << tableInfo.infoId();
         }
         Envelope area(-20e6, -7e6, 20e6, 15e6, featureServiceInfo.spatialReference());
         task->createDefaultGenerateGeodatabaseParameters(area);
      }
   });
   // Default parameters generated ? => generate the offline geodatabase
   connect(task, &GeodatabaseSyncTask::defaultGenerateGeodatabaseParametersCompleted,
         this, [this](QUuid, GenerateGeodatabaseParameters parameters) {

      generate(parameters);
     });

   
task->load();
}

void myClass::generate(GenerateGeodatabaseParameters params) {
   params.setSyncModel(SyncModel::Layer);

   QList<GenerateLayerOption> glo = params.layerOptions();
   for (int i = m_numLayersInService; i < glo.size(); ++i) {
      glo.setUseGeometry(false);
      glo.setWhereClause("1=1");
   }
   params.setLayerOptions(glo);
}

0 Kudos