Caricare dati in un Data Bundle
Questa guida spiega come caricare i file di input grezzi di un’acquisizione in un data bundle tramite la RealityConnect API.
Tutti gli endpoint di caricamento richiedono lo scope write:bundle; elencare le sessioni richiede read:bundle.
Flusso di caricamento
Sezione intitolata “Flusso di caricamento”Un data bundle contiene gli input grezzi di una singola acquisizione: una nuvola di punti, una traiettoria, immagini, metadati per immagine e così via. L’insieme esatto di file che un bundle accetta dipende dal dispositivo per cui il bundle è stato creato.
I file vengono caricati in sessioni di caricamento. Una sessione è un contenitore creato dal server per un batch di file di un bundle. Quando la apri dichiari quanti file conterrà il batch (expectedFileCount); la sessione si completa automaticamente una volta finalizzato quel numero di file.
Ogni singolo file viene caricato con un caricamento multipart di S3:
- Apri una sessione sul bundle, dichiarando il numero totale di file che invierai.
- Per ogni file: avvia il caricamento, esegui il PUT delle sue parti direttamente agli URL restituiti, quindi finalizzalo.
- Quando il numero di file finalizzati raggiunge
expectedFileCount, la sessione viene contrassegnata comecompleted.
sequenceDiagram
participant Client as Client
participant RCAPI as RCAPI
participant S3 as S3
Client->>RCAPI: POST upload-sessions
RCAPI-->>Client: sessionId + requirements
loop for each file
Client->>RCAPI: POST initiate file
RCAPI-->>Client: fileId + presigned part URLs
loop for each part
Client->>S3: PUT part
S3-->>Client: ETag
end
Client->>RCAPI: POST finalize
RCAPI-->>Client: file completed + sessionStatus
end
I requirements di caricamento (restituiti quando apri una sessione) ti indicano esattamente quali tipi di file il bundle accetta, quali sono obbligatori e come dipendono l’uno dall’altro.
L’esempio pratico: un’acquisizione ZF SLAM
Sezione intitolata “L’esempio pratico: un’acquisizione ZF SLAM”Un bundle ZF SLAM è un buon esempio perché accetta quattro diversi tipi di file con una dipendenza tra tipi. Un’acquisizione ZF completa è composta da:
| File | Tipo | Note |
|---|---|---|
scan.las | nuvola di punti | esattamente una |
…laser_0.txt | traiettoria | esattamente una |
panorama/*.jpg | immagini panoramiche | molte, ciascuna abbinata ai metadati |
panorama/*.json | metadati per immagine | molti, uno per immagine |
Quindi un batch ZF completo è 1 nuvola di punti + 1 traiettoria + N immagini + N file di metadati. Li caricheremo tutti in un’unica sessione.
Endpoint
Sezione intitolata “Endpoint”Tutti i percorsi sono relativi al tuo api_url regionale. Il bundle è indirizzato dal proprio id (bundleId).
| Metodo | Percorso | Scope | Scopo |
|---|---|---|---|
POST | /v1/bundles/{bundleId}/upload-sessions | write:bundle | Aprire una sessione |
GET | /v1/bundles/{bundleId}/upload-sessions | read:bundle | Elencare le sessioni (ripresa / progressi) |
POST | /v1/bundles/{bundleId}/upload-sessions/{sessionId}/files | write:bundle | Avviare il caricamento di un file |
GET | /v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/{startIndex} | write:bundle | Riemettere gli URL delle parti scaduti |
POST | /v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalize | write:bundle | Finalizzare il caricamento di un file |
Prima di iniziare
Sezione intitolata “Prima di iniziare”Per caricare dati in un data bundle, hai bisogno di:
- un flusso OAuth completato con un
access_tokene il tuoapi_urlregionale - un data bundle esistente (
bundleId)
Se non hai ancora un bundle, creane prima uno seguendo Creare un Data Bundle. Quel flusso recupera l’albero degli input del dispositivo, sceglie un dbuPath foglia e crea il bundle sotto un nodo padre.
Passaggio 1: aprire una sessione di caricamento
Sezione intitolata “Passaggio 1: aprire una sessione di caricamento”Dichiara il numero totale di file che il batch conterrà. Per un’acquisizione ZF completa con 13 immagini è 1 + 1 + 13 + 13 = 28.
POST {api_url}/v1/bundles/{bundleId}/upload-sessionsAuthorization: Bearer {access_token}Content-Type: application/json
{ "expectedFileCount": 28 }La risposta restituisce il sessionId e i requirements del bundle:
{ "sessionId": "6ebf0fe5-4428-4b26-a7e4-e3274980123b", "expectedFileCount": 28, "requirements": [ { "type": "pointclouds", "allowedExtensions": ["las", "laz"], "optional": false, "allowMultiples": false, "dependencies": [] }, { "type": "trajectories", "allowedExtensions": ["txt"], "optional": false, "allowMultiples": false, "dependencies": [] }, { "type": "pictures", "allowedExtensions": ["jpg", "png", "jpeg"], "optional": true, "allowMultiples": true, "dependencies": ["picturesMetadata"] }, { "type": "picturesMetadata", "allowedExtensions": ["json"], "optional": true, "allowMultiples": true, "dependencies": ["pictures"] } ]}Comprendere requirements e le dipendenze tra tipi
Sezione intitolata “Comprendere requirements e le dipendenze tra tipi”Ogni voce in requirements descrive un tipo di file che il bundle accetta:
| Campo | Significato |
|---|---|
type | L’identificatore che passi come type quando avvii il caricamento di un file. |
allowedExtensions | Estensioni di file accettate per questo tipo. |
optional | Se false, il bundle non può essere elaborato senza almeno un file di questo tipo. |
allowMultiples | Se false, è possibile caricare solo un file di questo tipo. |
dependencies | Altri tipi che devono essere presenti ogni volta che questo tipo è presente. |
Per l’esempio ZF:
pointcloudsetrajectoriessono obbligatori (optional: false) e singoli (allowMultiples: false). Ogni bundle ZF ne richiede esattamente uno di ciascuno.picturesepicturesMetadatasono opzionali e accettano più file.- I due tipi di immagine dipendono l’uno dall’altro:
pictures.dependencies = ["picturesMetadata"]epicturesMetadata.dependencies = ["pictures"].
Una dipendenza significa: se un tipo è presente, ogni tipo che elenca deve essere presente anch’esso. Poiché i tipi di immagine ZF si riferiscono l’uno all’altro, sono tutto-o-niente come coppia. Puoi caricare le immagini e i loro metadati insieme, oppure ometterli entrambi, ma non puoi caricarne uno senza l’altro.
Batch ZF validi:
- Minimo —
1 pointcloud + 1 trajectory(nessuna immagine).expectedFileCount = 2. - Completo —
1 pointcloud + 1 trajectory + N pictures + N picturesMetadata.expectedFileCount = 2 + 2N.
Non valido:
- Immagini senza i loro metadati (o viceversa) poiché la dipendenza tra tipi non è soddisfatta.
- Un batch senza nuvola di punti o senza traiettoria poiché manca un tipo obbligatorio.
Prima di aprire una sessione, conta ogni file che intendi caricare e imposta quel totale come expectedFileCount. I tipi opzionali contano solo se li includi nel batch. La sessione si completa quando quel numero di file viene finalizzato, quindi il numero che dichiari deve corrispondere a ciò che carichi effettivamente.
Passaggio 2: caricare ogni file
Sezione intitolata “Passaggio 2: caricare ogni file”Ripeti le tre chiamate seguenti per ogni file del batch.
2a. Avvio
Sezione intitolata “2a. Avvio”Passa il nome del file, la sua dimensione in byte e il suo type (uno dei tipi di requirements):
POST {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/filesAuthorization: Bearer {access_token}Content-Type: application/json
{ "fileName": "scan.las", "fileSize": 734003200, "type": "pointclouds" }La risposta descrive il caricamento multipart: quante parts inviare e un url prefirmato per parte.
{ "fileId": "c8c8cb0b-f476-4211-a3a4-72e4e04b91d5", "parts": 8, "urls": [ { "partNumber": 1, "url": "https://s3…/part-1?…" }, { "partNumber": 2, "url": "https://s3…/part-2?…" } ]}2b. Caricare le parti
Sezione intitolata “2b. Caricare le parti”Suddividi il file in parts blocchi sequenziali di ceil(fileSize / parts) byte ed esegui il PUT di ogni blocco al suo url prefirmato. Su queste richieste non viene inviato alcun header di autenticazione; l’URL è già firmato. Conserva l’header di risposta ETag di ogni parte, ti serve per la finalizzazione.
import math, requests
def upload_parts(file_path, file_size, urls): part_size = math.ceil(file_size / len(urls)) parts = [] with open(file_path, "rb") as handle: for entry in sorted(urls, key=lambda u: u["partNumber"]): res = requests.put(entry["url"], data=handle.read(part_size)) res.raise_for_status() parts.append({"partNumber": entry["partNumber"], "etag": res.headers["ETag"]}) return partsPassa l’ETag esattamente come restituito, comprese le virgolette che lo racchiudono.
2c. Finalizzare
Sezione intitolata “2c. Finalizzare”Invia le parti raccolte per completare il file. L’endpoint restituisce 200 OK.
POST {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/finalizeAuthorization: Bearer {access_token}Content-Type: application/json
{ "parts": [ { "partNumber": 1, "etag": "\"a1b2…\"" }, { "partNumber": 2, "etag": "\"c3d4…\"" } ] }La risposta riporta il file e comunica lo stato della sessione:
{ "fileId": "c8c8cb0b-f476-4211-a3a4-72e4e04b91d5", "fileName": "scan.las", "type": "pointclouds", "size": 734003200, "status": "completed", "sessionStatus": "pending"}sessionStatus rimane pending finché non viene finalizzato l’ultimo file previsto, momento in cui diventa completed.
Rinnovare gli URL scaduti
Sezione intitolata “Rinnovare gli URL scaduti”Gli URL prefirmati scadono. Se un caricamento dura a lungo e un PUT inizia a restituire 403, riemetti gli URL a partire dalla parte in cui ti sei fermato e continua:
GET {api_url}/v1/bundles/{bundleId}/upload-sessions/{sessionId}/files/{fileId}/refresh/0Authorization: Bearer {access_token}{ "startIndex": 0, "urls": [ { "partNumber": 1, "url": "https://s3…/part-1?…" } ] }Il file non deve essere riavviato; vengono rinnovati solo gli URL delle parti.
Passaggio 3: monitorare i progressi e riprendere
Sezione intitolata “Passaggio 3: monitorare i progressi e riprendere”Elenca le sessioni di un bundle per verificare i progressi o riprendere un batch interrotto senza mantenere il sessionId in memoria. L’elenco è paginato e ordinabile, con un filtro status opzionale.
Parametri di query: page (predefinito 1), limit (predefinito 20, massimo 20), sortBy (createdAt o status, predefinito createdAt), sortDir (ASC o DESC, predefinito ASC), status (filtro opzionale: pending o completed).
GET {api_url}/v1/bundles/{bundleId}/upload-sessions?page=1&limit=20&sortBy=createdAt&sortDir=DESCAuthorization: Bearer {access_token}{ "items": [ { "sessionId": "6ebf0fe5-4428-4b26-a7e4-e3274980123b", "status": "pending", "expectedFileCount": 28, "finalizedFileCount": 14 } ], "total": 1}finalizedFileCount rispetto a expectedFileCount ti indica quanti file rimangono. Per riprendere, avvia e finalizza solo i file non ancora completati.
Validazione e limiti
Sezione intitolata “Validazione e limiti”Leggi attentamente questa sezione perché determina di cosa sei responsabile quando usi la RealityConnect API per caricare dati.
- Gli endpoint di caricamento registrano ogni file sotto il
typeche fornisci e completano la sessione puramente in base al conteggio: una volta chefinalizedFileCountè uguale aexpectedFileCount, la sessione ècompleted. Al momento del caricamento, il server non verifica che iltypesia uno deirequirements, che l’estensione del file sia consentita, che i tipi obbligatori siano presenti o che le dipendenze tra tipi siano soddisfatte. - Tutte le regole (tipi obbligatori,
allowMultiplese ledependenciestra i tipi) vengono applicate dopo che il bundle è stato inviato per l’elaborazione. Un batch che ignora irequirementsverrà caricato correttamente ma non potrà essere elaborato. - La validazione approfondita del contenuto non viene mai eseguita dall’API di caricamento. Se un
.lasè una vera nuvola di punti ZF, o se un.jsonfa correttamente riferimento alla sua immagine, viene verificato solo in fase di elaborazione.
Considera i requirements come il contratto. Rispetta i tipi, le estensioni, l’opzionalità, la molteplicità e le dipendenze nel tuo batch di caricamento affinché il bundle sia elaborabile una volta caricato. Gli errori di elaborazione causati da una struttura di caricamento errata non sono rimborsabili.
Esempio completo
Sezione intitolata “Esempio completo”Mettendo tutto insieme per l’acquisizione ZF. files è l’elenco che costruisci dal tuo dataset locale, con ogni voce che abbina un percorso al suo type da requirements.
import math, requests
API_URL = "https://<your-regional-api-url>"TOKEN = "<access_token>"BUNDLE_ID = "<bundleId>"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
# (percorso locale, tipo di schema) per ogni file del batchfiles = [ ("scan.las", "pointclouds"), ("2024-07-scan.laser_laser_0.txt", "trajectories"), ("panorama/2024-07-scan-idx40.jpg", "pictures"), ("panorama/2024-07-scan-idx40.json", "picturesMetadata"), # … coppie immagine / metadati rimanenti …]
def upload_one(session_id, path, file_type): size = __import__("os").path.getsize(path) base = f"{API_URL}/v1/bundles/{BUNDLE_ID}/upload-sessions/{session_id}/files"
initiated = requests.post( base, headers=HEADERS, json={"fileName": path.split("/")[-1], "fileSize": size, "type": file_type}, ).json()
part_size = math.ceil(size / initiated["parts"]) parts = [] with open(path, "rb") as handle: for entry in sorted(initiated["urls"], key=lambda u: u["partNumber"]): res = requests.put(entry["url"], data=handle.read(part_size)) res.raise_for_status() parts.append({"partNumber": entry["partNumber"], "etag": res.headers["ETag"]})
return requests.post( f"{base}/{initiated['fileId']}/finalize", headers=HEADERS, json={"parts": parts}, ).json()
# 1. Apri la sessione per il numero esatto di file che invieremo.session = requests.post( f"{API_URL}/v1/bundles/{BUNDLE_ID}/upload-sessions", headers=HEADERS, json={"expectedFileCount": len(files)},).json()
# 2. Carica ogni file sotto il suo tipo.result = Nonefor path, file_type in files: result = upload_one(session["sessionId"], path, file_type)
# 3. L'ultima finalizzazione riporta la sessione come completata.assert result["sessionStatus"] == "completed"Passaggio successivo
Sezione intitolata “Passaggio successivo”Una volta caricato ogni file e completata la sessione (completed), continua con Elaborazione e monitoraggio per trasformare gli input grezzi in output visualizzabili.