46 lines
4.6 KiB
PHP
46 lines
4.6 KiB
PHP
<?php
|
|
namespace App\Shared\Geography;
|
|
|
|
use InvalidArgumentException;
|
|
use DomainException;
|
|
|
|
/** Versioned preparation of a supplied territorial reference, without changing business records. */
|
|
final class Cartography
|
|
{
|
|
public const FIELDS=['province','district','administrative_post','locality','urban_neighbourhood','village_quarter'];
|
|
public const HEADERS=['A'=>'ID','B'=>'Provincia','C'=>'Distrito','D'=>'Posto_Administrativo','E'=>'Localidade','F'=>'Bairro_Urbano','G'=>'Aldeia_Quarteirao','H'=>'Designacao'];
|
|
|
|
public static function prepare(array $source):array
|
|
{
|
|
if(!preg_match('/^[a-f0-9]{64}$/D',$source['sha256']??'')||($source['rows'][0]['cells']??null)!==self::HEADERS||($source['formula_cells']??1)!==0)throw new InvalidArgumentException('CARTOGRAPHY_SOURCE_REFUSED');
|
|
$nodes=[];$paths=[];$ids=[];$levels=array_fill(1,6,0);
|
|
foreach(array_slice($source['rows'],1)as$row){
|
|
$c=$row['cells'];$id=$c['A']??'';if(!is_string($id)||!preg_match('/^[0-9]+$/D',$id)||isset($ids[$id]))throw new InvalidArgumentException('CARTOGRAPHY_SOURCE_ID_REFUSED');$ids[$id]=true;
|
|
$parts=[];foreach(['B','C','D','E','F','G']as$col){$code=$c[$col]??'';if(!is_string($code)||($code!==''&&!preg_match('/^[A-Za-z0-9]+$/D',$code)))throw new InvalidArgumentException('CARTOGRAPHY_CODE_REFUSED');$parts[]=$code;}
|
|
$level=0;foreach($parts as$i=>$code)if($code!=='')$level=$i+1;
|
|
if(!$level||in_array('',array_slice($parts,0,$level),true))throw new InvalidArgumentException('CARTOGRAPHY_PATH_REFUSED');
|
|
$path=implode('.',$parts);$key=$source['sha256'].':'.$id;$index=count($nodes);$paths[$path][]=$index;$levels[$level]++;
|
|
$nodes[]=['key'=>$key,'source_id'=>$id,'source_row'=>$row['row'],'level'=>$level,'designation'=>$c['H']??null,'codes'=>array_combine(self::FIELDS,$parts),'path'=>$path,'parent_candidates'=>[],'issues'=>[]];
|
|
}
|
|
$summary=['duplicate_path_groups'=>0,'duplicate_extra_rows'=>0,'conflicting_designation_groups'=>0,'missing_designations'=>0,'missing_parent_records'=>0,'ambiguous_parent_records'=>0,'incomplete_ancestry_records'=>0,'selectable_records'=>0];
|
|
foreach($paths as$group)if(count($group)>1){$summary['duplicate_path_groups']++;$summary['duplicate_extra_rows']+=count($group)-1;if(count(array_unique(array_map(fn($i)=>$nodes[$i]['designation'],$group)))>1)$summary['conflicting_designation_groups']++;}
|
|
foreach($nodes as&$node){
|
|
if($node['designation']===null||trim($node['designation'])===''){$node['issues'][]='missing_designation';$summary['missing_designations']++;}
|
|
if(count($paths[$node['path']])>1)$node['issues'][]='duplicate_path';
|
|
if($node['level']>1){$parts=array_values($node['codes']);$parts[$node['level']-1]='';$parents=$paths[implode('.',$parts)]??[];$node['parent_candidates']=array_map(fn($i)=>$nodes[$i]['key'],$parents);if(!$parents){$node['issues'][]='parent_record_missing';$summary['missing_parent_records']++;}elseif(count($parents)>1){$node['issues'][]='parent_record_ambiguous';$summary['ambiguous_parent_records']++;}}
|
|
$ancestry=true;for($depth=1;$depth<$node['level'];$depth++){$prefix=array_values($node['codes']);for($i=$depth;$i<6;$i++)$prefix[$i]='';$ancestors=$paths[implode('.',$prefix)]??[];if(count($ancestors)!==1||trim($nodes[$ancestors[0]]['designation']??'')===''){$ancestry=false;break;}}
|
|
if(!$ancestry){$node['issues'][]='ancestry_requires_review';$summary['incomplete_ancestry_records']++;}
|
|
$node['selectable']=$node['issues']===[];if($node['selectable'])$summary['selectable_records']++;
|
|
}unset($node);
|
|
return ['format'=>'sgf.cartography.staging.v1','source'=>['name'=>$source['source_name'],'sha256'=>$source['sha256'],'sheet'=>'Sheet1','range'=>$source['dimension'],'authority_declared_by_user'=>'INE','authority_independently_verified'=>false],'status'=>'prepared_for_review','levels'=>$levels,'total'=>count($nodes),'diagnostics'=>$summary,'nodes'=>$nodes];
|
|
}
|
|
|
|
/** A human selection carries version and source identity; a name alone cannot select a territory. */
|
|
public static function selection(array $catalogue,string $version,string $key):array
|
|
{
|
|
if(!hash_equals($catalogue['source']['sha256'],$version))throw new DomainException('CARTOGRAPHY_VERSION_CHANGED');
|
|
foreach($catalogue['nodes']as$node)if($node['key']===$key){if(!$node['selectable'])throw new DomainException('CARTOGRAPHY_REVIEW_REQUIRED');return ['version'=>$version,'territory_key'=>$key,'source_id'=>$node['source_id'],'codes'=>$node['codes'],'designation'=>$node['designation']];}
|
|
throw new DomainException('CARTOGRAPHY_TERRITORY_UNKNOWN');
|
|
}
|
|
}
|