Improve MineGeo exploration and mapping features

This commit is contained in:
2026-10-05 13:41:36 +05:00
parent aec0641f87
commit c5368fa8cc
69 changed files with 3581 additions and 333 deletions

View File

@@ -0,0 +1,172 @@
-- Contributions reuse the private upload pipeline. Only root maps can be published.
alter table public.datasets
add column parent_dataset_id uuid references public.datasets(id),
add column allow_contributions boolean not null default false,
add column contribution_status text check (contribution_status in ('pending','submitted','accepted','rejected')),
add column contribution_count integer not null default 0 check (contribution_count between 0 and 24),
add column update_state text check (update_state in ('draft','review','rejected'));
create index datasets_contribution_parent on public.datasets(parent_dataset_id,owner_id) where parent_dataset_id is not null;
create function public.contribution_capacity(p_dataset uuid,p_owner uuid) returns jsonb
language plpgsql security invoker set search_path='' as $$
declare d public.datasets; m jsonb; reserved integer; used integer; pool integer;
begin
select * into d from public.datasets where id=p_dataset for update;
if not found or d.parent_dataset_id is not null or d.state not in ('draft','rejected','published') then raise exception 'Map does not accept contributions at this time'; end if;
if d.owner_id<>p_owner and not d.allow_contributions then raise exception 'The owner has disabled player contributions'; end if;
select manifest into m from public.dataset_inputs where dataset_id=p_dataset;
pool:=jsonb_array_length(coalesce(m->'availableViewpoints',m->'viewpoints'));
select coalesce(sum(contribution_count) filter(where contribution_status in ('pending','submitted') and state<>'failed'),0),
coalesce(sum(contribution_count) filter(where owner_id=p_owner and (contribution_status='accepted' or (contribution_status in ('pending','submitted') and state<>'failed'))),0)
into reserved,used from public.datasets where parent_dataset_id=p_dataset;
return jsonb_build_object('id',d.id,'name',d.name,'bounds',d.bounds,'remaining',greatest(0,24-pool-reserved),
'playerRemaining',greatest(0,case when d.owner_id=p_owner then 24 else 12 end-used),'isOwner',d.owner_id=p_owner);
end; $$;
create function public.create_upload(p_owner uuid,p_manifest jsonb) returns uuid
language plpgsql security invoker set search_path='' as $$
declare target uuid; capacity jsonb; server uuid; result uuid; count integer; b jsonb;
begin
target:=(p_manifest->>'contributionTo')::uuid;
count:=jsonb_array_length(p_manifest->'viewpoints');
if count<1 or count>24 or (target is null and count<5) then raise exception 'Maps require 5–24 panoramas'; end if;
if exists(select 1 from jsonb_array_elements(p_manifest->'viewpoints') p where p ?| array['assetRoot','assetId','contributorId','source']) or p_manifest ? 'availableViewpoints' then raise exception 'Upload contains server-only media provenance'; end if;
if target is not null then
capacity:=public.contribution_capacity(target,p_owner); b:=capacity->'bounds';
if count>(capacity->>'remaining')::integer then raise exception 'Map limit is 24 panoramas, including pending uploads'; end if;
if count>(capacity->>'playerRemaining')::integer then raise exception 'Each player may contribute at most 12 panoramas per map'; end if;
if exists(select 1 from jsonb_array_elements(p_manifest->'viewpoints') p where
(p->>'x')::numeric<(b->>'minX')::numeric or (p->>'x')::numeric>(b->>'maxX')::numeric or
(p->>'z')::numeric<(b->>'minZ')::numeric or (p->>'z')::numeric>(b->>'maxZ')::numeric) then raise exception 'Contributed panoramas must be inside the original map bounds'; end if;
select server_id into server from public.datasets where id=target;
else
select id into server from public.catalog_servers where name=p_manifest->>'serverName' limit 1;
if server is null then insert into public.catalog_servers(name,address) values(p_manifest->>'serverName',p_manifest->>'serverAddress') returning id into server; end if;
end if;
insert into public.datasets(owner_id,server_id,name,description,source,bounds,parent_dataset_id,contribution_status,contribution_count)
values(p_owner,server,p_manifest->>'name',coalesce(p_manifest->>'description',''),p_manifest->>'source',p_manifest->'bounds',target,
case when target is not null then 'pending' end,case when target is not null then count else 0 end) returning id into result;
insert into public.dataset_inputs(dataset_id,manifest) values(result,p_manifest);
return result;
end; $$;
create or replace function public.complete_job(p_job uuid,p_worker text,p_count integer,p_cover text) returns void
language plpgsql security invoker set search_path='' as $$
declare j public.upload_jobs; d public.datasets;
begin
select * into j from public.upload_jobs where id=p_job and lease_owner=p_worker and state='running' and lease_until>now() for update;
if not found then raise exception 'Worker lease lost'; end if;
select * into d from public.datasets where id=j.dataset_id for update;
if p_count<(case when d.parent_dataset_id is null then 5 else 1 end) or p_count>24 then raise exception 'Invalid panorama count'; end if;
update public.datasets set state='draft',panorama_count=p_count,map_path=j.dataset_id::text||'/map.webp',cover_path=j.dataset_id::text||'/'||p_cover||'/preview.webp',updated_at=now() where id=j.dataset_id and state='processing';
if not found then raise exception 'Dataset processing cancelled'; end if;
update public.dataset_inputs set validated=true where dataset_id=j.dataset_id;
update public.upload_jobs set state='complete',progress=100,lease_until=null,error=null,updated_at=now() where id=p_job;
end; $$;
-- Lock before selecting locations, so acceptance cannot race with owner edits.
create function public.save_draft(p_dataset uuid,p_owner uuid,p_name text,p_description text,p_ids jsonb) returns void
language plpgsql security invoker set search_path='' as $$
declare d public.datasets; m jsonb; available jsonb; selected jsonb; count integer; first jsonb;
begin
select * into d from public.datasets where id=p_dataset and owner_id=p_owner for update;
if not found then raise exception 'Dataset not found'; end if;
if d.state not in ('draft','rejected','published') or d.update_state='review' or d.contribution_status in ('submitted','accepted','rejected') then raise exception 'Draft cannot be edited while awaiting approval'; end if;
if length(p_name)<3 or length(p_name)>80 or length(p_description)>2000 then raise exception 'Invalid map details'; end if;
select manifest into m from public.dataset_inputs where dataset_id=p_dataset and validated;
if m is null then raise exception 'Dataset not validated'; end if;
available:=coalesce(m->'availableViewpoints',m->'viewpoints');
select coalesce(jsonb_agg(p order by n),'[]') into selected from jsonb_array_elements(available) with ordinality as pts(p,n) where p_ids ? (p->>'id');
count:=jsonb_array_length(selected);
if count<>jsonb_array_length(p_ids) or count<(case when d.parent_dataset_id is null then 5 else 1 end) or count>24 then raise exception 'Choose unique captured locations within the panorama limit'; end if;
m:=m||jsonb_build_object('name',p_name,'description',p_description,'availableViewpoints',available,'viewpoints',selected);
update public.dataset_inputs set manifest=m where dataset_id=p_dataset;
first:=selected->0;
update public.datasets set name=case when state='published' then name else p_name end,
description=case when state='published' then description else p_description end,
panorama_count=case when state='published' then panorama_count else count end,
cover_path=case when state='published' then cover_path else coalesce(first->>'assetRoot',id::text)||'/'||coalesce(first->>'assetId',first->>'id')||'/preview.webp' end,
state=case when state='published' then state else 'draft' end,
update_state=case when state='published' then 'draft' else null end,rejection_reason=null,updated_at=now() where id=p_dataset;
end; $$;
create function public.submit_draft(p_dataset uuid,p_owner uuid) returns void
language plpgsql security invoker set search_path='' as $$
declare d public.datasets; count integer;
begin
select * into d from public.datasets where id=p_dataset and owner_id=p_owner for update;
if not found then raise exception 'Dataset not found'; end if;
if d.state not in ('draft','rejected','published') or (d.state='published' and coalesce(d.update_state,'') not in ('draft','rejected')) or d.contribution_status in ('submitted','accepted','rejected') then raise exception 'Draft is not ready for submission'; end if;
select jsonb_array_length(manifest->'viewpoints') into count from public.dataset_inputs where dataset_id=p_dataset and validated;
if count is null or count<(case when d.parent_dataset_id is null then 5 else 1 end) or count>24 then raise exception 'Invalid panorama count'; end if;
update public.datasets set state=case when state='published' then state else 'review' end,
update_state=case when state='published' then 'review' end,
contribution_status=case when parent_dataset_id is not null then 'submitted' end,updated_at=now() where id=p_dataset;
end; $$;
create function public.set_contributions(p_dataset uuid,p_owner uuid,p_allowed boolean) returns void
language plpgsql security invoker set search_path='' as $$
begin
perform 1 from public.datasets where id=p_dataset and owner_id=p_owner and parent_dataset_id is null for update;
if not found then raise exception 'Map owner permission required'; end if;
update public.datasets set allow_contributions=p_allowed,updated_at=now() where id=p_dataset;
end; $$;
create function public.review_contribution(p_dataset uuid,p_contribution uuid,p_owner uuid,p_decision text,p_reason text default null) returns void
language plpgsql security invoker set search_path='' as $$
declare d public.datasets; c public.datasets; m jsonb; cm jsonb; added jsonb; pool jsonb; chosen jsonb;
begin
select * into d from public.datasets where id=p_dataset and owner_id=p_owner and parent_dataset_id is null for update;
if not found then raise exception 'Map owner permission required'; end if;
if d.state not in ('draft','rejected','published') or d.update_state='review' then raise exception 'Wait until the current map review finishes'; end if;
select * into c from public.datasets where id=p_contribution and parent_dataset_id=p_dataset for update;
if not found or c.contribution_status<>'submitted' or c.state<>'review' then raise exception 'Contribution is not awaiting owner approval'; end if;
if p_decision='reject' then
if coalesce(length(trim(p_reason)),0)=0 then raise exception 'Provide feedback for the contributor'; end if;
update public.datasets set state='rejected',contribution_status='rejected',rejection_reason=p_reason,updated_at=now() where id=c.id;
return;
end if;
if p_decision<>'accept' then raise exception 'Invalid contribution decision'; end if;
select manifest into cm from public.dataset_inputs where dataset_id=c.id and validated;
select manifest into m from public.dataset_inputs where dataset_id=d.id and validated;
if cm is null or m is null then raise exception 'Dataset not validated'; end if;
select jsonb_agg(p||jsonb_build_object('id','c'||replace(c.id::text,'-','')||'_'||n,'assetRoot',c.id,'assetId',p->>'id','contributorId',c.owner_id,'source',c.source) order by n)
into added from jsonb_array_elements(cm->'viewpoints') with ordinality as pts(p,n);
pool:=coalesce(m->'availableViewpoints',m->'viewpoints')||added;
if jsonb_array_length(pool)>24 then raise exception 'Map limit is 24 panoramas'; end if;
if c.owner_id<>d.owner_id and (select coalesce(sum(contribution_count),0) from public.datasets where parent_dataset_id=d.id and owner_id=c.owner_id and contribution_status='accepted')+jsonb_array_length(added)>12 then raise exception 'Each player may contribute at most 12 panoramas per map'; end if;
chosen:=(m->'viewpoints')||added;
update public.dataset_inputs set manifest=m||jsonb_build_object('availableViewpoints',pool,'viewpoints',chosen) where dataset_id=d.id;
update public.datasets set state=case when state='published' then state else 'draft' end,
update_state=case when state='published' then 'draft' end,
panorama_count=case when state='published' then panorama_count else jsonb_array_length(chosen) end,rejection_reason=null,updated_at=now() where id=d.id;
update public.datasets set contribution_status='accepted',contribution_count=jsonb_array_length(added),updated_at=now() where id=c.id;
end; $$;
create or replace function public.publish_dataset(p_dataset uuid,p_reviewer uuid,p_decision text,p_reason text default null) returns void
language plpgsql security invoker set search_path='' as $$
declare d public.datasets; m jsonb; rev uuid; first jsonb; count integer;
begin
if not exists(select 1 from public.profiles where id=p_reviewer and role='moderator') then raise exception 'Moderator permission required'; end if;
select * into d from public.datasets where id=p_dataset and parent_dataset_id is null for update;
if not found then raise exception 'Map not found'; end if;
if p_decision='approve' then
if not(d.state='review' or (d.state='published' and coalesce(d.update_state,'')='review')) then raise exception 'Map is not ready for publication'; end if;
select manifest into m from public.dataset_inputs where dataset_id=p_dataset and validated;
count:=jsonb_array_length(m->'viewpoints'); first:=m->'viewpoints'->0;
if m is null or count<5 or count>24 then raise exception 'Map requires 5–24 validated panoramas'; end if;
insert into public.revisions(dataset_id,manifest,asset_root) values(p_dataset,m,p_dataset::text) returning id into rev;
update public.datasets set state='published',revision_id=rev,update_state=null,name=m->>'name',description=m->>'description',panorama_count=count,
cover_path=coalesce(first->>'assetRoot',id::text)||'/'||coalesce(first->>'assetId',first->>'id')||'/preview.webp',rejection_reason=null,updated_at=now() where id=p_dataset;
elsif p_decision='reject' and (d.state='review' or (d.state='published' and d.update_state='review')) then
if coalesce(length(trim(p_reason)),0)=0 then raise exception 'Provide a rejection reason'; end if;
update public.datasets set state=case when state='published' then state else 'rejected' end,update_state=case when state='published' then 'rejected' end,rejection_reason=p_reason,updated_at=now() where id=p_dataset;
elsif p_decision='unpublish' and d.state='published' then
update public.datasets set state='unpublished',update_state=null,updated_at=now() where id=p_dataset;
else raise exception 'Invalid review transition'; end if;
insert into public.review_decisions(dataset_id,reviewer_id,decision,reason) values(p_dataset,p_reviewer,p_decision,p_reason);
end; $$;
revoke execute on function public.contribution_capacity(uuid,uuid),public.create_upload(uuid,jsonb),public.save_draft(uuid,uuid,text,text,jsonb),public.submit_draft(uuid,uuid),public.set_contributions(uuid,uuid,boolean),public.review_contribution(uuid,uuid,uuid,text,text) from public,anon,authenticated;
grant execute on function public.contribution_capacity(uuid,uuid),public.create_upload(uuid,jsonb),public.save_draft(uuid,uuid,text,text,jsonb),public.submit_draft(uuid,uuid),public.set_contributions(uuid,uuid,boolean),public.review_contribution(uuid,uuid,uuid,text,text) to service_role;