41 lines
4.9 KiB
PHP
41 lines
4.9 KiB
PHP
<?php
|
|
require_once __DIR__ . '/labdb.class.php';
|
|
require_once __DIR__ . '/mapcatalog.class.php';
|
|
final class Maps {
|
|
private static function error(string $field,string $message): array { return ['ok'=>false,'errors'=>[$field=>$message]]; }
|
|
public static function all(string $resolution): array {
|
|
$schema=MapCatalog::schema();if(!$schema||!isset($schema['grids'][$resolution]))return self::error('resolution','Unsupported map resolution.');
|
|
$q=LabDb::connection()->prepare('SELECT id,field,resolution,name,rows,cols,digest,created_at FROM map_versions WHERE owner=? AND resolution=? ORDER BY created_at DESC LIMIT 200');$q->execute([lab_config()['workspace'],$resolution]);
|
|
return ['ok'=>true,'data'=>['maps'=>$q->fetchAll(),'fields'=>$schema['fields'],'grid'=>$schema['grids'][$resolution]]];
|
|
}
|
|
public static function save(string $field,string $resolution,string $name,string $bytes): array {
|
|
$schema=MapCatalog::schema();$contract=$schema['fields'][$field]??null;$shape=$schema['grids'][$resolution]??null;
|
|
if(!$contract||!$shape)return self::error('field','Unsupported map field or resolution.');
|
|
if(strlen($name)>120||trim($name)==='')return self::error('name','A map name is required (maximum 120 bytes).');
|
|
$rows=(int)$shape['nlat'];$cols=(int)$shape['nlon'];$count=$rows*$cols;if(strlen($bytes)!==$count*4)return self::error('map','Map byte count does not match its grid.');
|
|
$values=unpack('g*',$bytes);if(!$values||count($values)!==$count)return self::error('map','Map data is invalid.');
|
|
foreach($values as $value)if(!is_finite($value)||$value<$contract['min']||$value>$contract['max'])return self::error('map','Map values must be finite and within the catalog range.');
|
|
if(!empty($contract['categorical']))foreach($values as $value)if($value!==0.0&&$value!==1.0)return self::error('map','Categorical map values must be 0 or 1.');
|
|
$id=bin2hex(random_bytes(16));$digest=hash('sha256',$bytes);$db=LabDb::connection();$q=$db->prepare('INSERT INTO map_versions(id,owner,field,resolution,name,rows,cols,digest,data,created_at) VALUES(?,?,?,?,?,?,?,?,?,?)');
|
|
$q->bindValue(1,$id);$q->bindValue(2,lab_config()['workspace']);$q->bindValue(3,$field);$q->bindValue(4,$resolution);$q->bindValue(5,trim($name));$q->bindValue(6,$rows,PDO::PARAM_INT);$q->bindValue(7,$cols,PDO::PARAM_INT);$q->bindValue(8,$digest);$q->bindValue(9,$bytes,PDO::PARAM_LOB);$q->bindValue(10,gmdate('c'));$q->execute();
|
|
return ['ok'=>true,'data'=>['id'=>$id,'field'=>$field,'resolution'=>$resolution,'name'=>trim($name),'shape'=>[$rows,$cols],'digest'=>$digest]];
|
|
}
|
|
public static function get(string $id): ?array { if(!preg_match('/^[a-f0-9]{32}$/',$id))return null;$q=LabDb::connection()->prepare('SELECT id,field,resolution,name,rows,cols,digest,data,created_at FROM map_versions WHERE id=? AND owner=?');$q->execute([$id,lab_config()['workspace']]);return $q->fetch()?:null; }
|
|
public static function validate(array $spec): array {
|
|
$maps=$spec['maps']??[];if(!is_array($maps)||(array_is_list($maps)&&$maps!==[]))return self::error('maps','Expected a map version object.');$schema=MapCatalog::schema();
|
|
foreach(array_diff(array_keys($maps),array_keys($schema['fields']??[])) as $key)return self::error('maps.'.$key,'Unsupported map field.');$resolution=$spec['model']['resolution']??'';
|
|
foreach($maps as $field=>$id){if(!is_string($id)||!($map=self::get($id)))return self::error('maps.'.$field,'Map version not found.');if($map['field']!==$field||$map['resolution']!==$resolution)return self::error('maps.'.$field,'Map field or resolution does not match the job.');if(!hash_equals($map['digest'],hash('sha256',$map['data'])))return self::error('maps.'.$field,'Stored map digest does not match.');}
|
|
return ['ok'=>true,'data'=>true];
|
|
}
|
|
public static function captures(array $spec): array {
|
|
$valid=self::validate($spec);if(empty($valid['ok']))return $valid;$captures=[];
|
|
foreach($spec['maps']??[] as $field=>$id){$map=self::get($id);$captures[$field]=['id'=>$id,'digest'=>$map['digest'],'data'=>$map['data']];}
|
|
return ['ok'=>true,'data'=>$captures];
|
|
}
|
|
public static function writeSnapshots(array $captures,string $jobDir): array {
|
|
if(!$captures)return ['ok'=>true,'data'=>[]];$target=$jobDir.'/inputs/maps';if(!is_dir($target)&&!mkdir($target,0750,true)&&!is_dir($target))return self::error('maps','Could not create job map snapshot.');$paths=[];
|
|
foreach($captures as $field=>$capture){$bytes=$capture['data']??'';if(!is_string($bytes)||!hash_equals((string)($capture['digest']??''),hash('sha256',$bytes)))return self::error('maps.'.$field,'Captured map digest does not match.');$path=$target.'/'.$field.'.f32';if(file_put_contents($path,$bytes,LOCK_EX)!==strlen($bytes))return self::error('maps.'.$field,'Could not write map snapshot.');$paths[$field]=$path;}
|
|
return ['ok'=>true,'data'=>$paths];
|
|
}
|
|
}
|