<resource schema="ivoa" resdir="__system">
	<meta name="creationDate">2021-11-11T13:00:00</meta>
	<meta name="title">\getConfig{web}{siteName} obs_radio Table</meta>
	<meta name="subject">observational-astronomy</meta>
	<meta name="subject">interferometry</meta>
	<meta name="description">Radio-specific metadata for suitable data
		products from \getConfig{web}{siteName}'s obscore table.
	</meta>

<!-- The following column definitions are created by machine from the
definitions in
git@github.com:ivoa-std/ObsCoreExtensionForRadioData.git
As of December 2023, there's clearly quite some work to do to improve the
column metadata; but that should be done in the spec, where I have
manually purged some particularly broken columns.

I've quickly hacked this program to do that:

import re

with open("ObsCoreExtensionForRadioData.tex") as f:
	src = f.read()

tabsrc = re.search(r"(?s)\\begin{longtable}(.*)\\end{longtable}", src
	).group(1)
for mat in re.finditer(r"\\texttt.*\\cr", tabsrc):
	name, definition, utype, ucd, unit = re.sub(
		r"\\[a-z]+|[{}]", "", mat.group(0)
		).replace(r"\_", "_").split("&")[:5]

	utype = utype.replace(" ", "")
	definition = definition[0].upper()+definition[1:]

	print(f'<column name="{name}"\n'
		f'\tunit="{unit}" ucd="{ucd}"\n'
		f'\tutype="{utype}"\n'
		f'\tdescription="{definition}">\n'
		'\t<property name="std">1</property>\n'
		'</column>')
-->

	<STREAM id="obs-radio-columns">
		<doc>
			The columns of the ObsCore radio extension.  You may want
			to replay this into a table intended to mix in //obs-radio#publish
			-- but there is nothing wrong with not doing so.
		</doc>

		<!-- that's our foreign key -->
		<column original="//obscore#ObsCore.obs_publisher_did"/>

		<column name="s_resolution_min"
			unit="arcsec" ucd="pos.angResolution;stat.min"
			utype="Char.SpatialAxis.Resolution.Bounds.Limits.LoLim"
			description="Angular resolution, longest baseline and  max frequency dependent">
			<property name="std">1</property>
		</column>
		<column name="s_resolution_max"
			unit="arcsec" ucd="pos.angResolution;stat.max"
			utype="Char.SpatialAxis.Resolution.Bounds.Limits.HiLim"
			description="Angular resolution, longest baseline and min frequency dependent">
			<property name="std">1</property>
		</column>
		<column name="s_fov_min"
			unit="deg" ucd="phys.angSize;instr.fov;stat.min"
			utype="Char.SpatialAxis.Coverage.Bounds.Extent.LowLim"
			description="Field of view diameter,  min value, max frequency dependent">
			<property name="std">1</property>
		</column>
		<column name="s_fov_max"
			unit="deg" ucd="phys.angSize;instr.fov;stat.max"
			utype="Char.SpatialAxis.Coverage.Bounds.Extent.HiLim"
			description="Field of view diameter,  max value, min frequency dependent">
			<property name="std">1</property>
		</column>
		<column name="s_maximum_angular_scale"
			unit="arcsec" ucd="phys.angSize;stat.max"
			utype="Char.SpatialAxis.Resolution.Scale.Limits.HiLim"
			description="Maximum scale in dataset, shortest baseline and  frequency dependent">
			<property name="std">1</property>
		</column>
		<column name="f_resolution"
			unit="kHz" ucd="em.freq;stat.max"
			utype="Char.SpectralAxis.Coverage.BoundsLimits.HiLim"
			description="Absolute spectral resolution in frequency">
			<property name="std">1</property>
		</column>
		<column name="t_exp_min"
			unit="s" ucd="time.duration;obs.exposure;stat.min"
			utype="Char.TimeAxis.Sampling.ExtentLoLim"
			description="Minimum integration time per sample">
			<property name="std">1</property>
		</column>
		<column name="t_exp_max"
			unit="s" ucd="time.duration;obs.exposure;stat.max"
			utype="Char.TimeAxis.Sampling.ExtentHiLim"
			description="Maximum integration time per sample">
			<property name="std">1</property>
		</column>
		<column name="t_exp_mean"
			unit="s" ucd="time.duration;obs.exposure;stat.mean"
			utype="Char.TimeAxis.Sampling.ExtentHiLim"
			description="Average integration time per sample">
			<property name="std">1</property>
		</column>
		<column name="uv_distance_min"
			unit="m" ucd="stat.fourier;pos;stat.min"
			utype="Char.UVAxis.Coverage.Bounds.Limits.LoLim"
			description="Minimal distance in uv plane">
			<property name="std">1</property>
		</column>
		<column name="uv_distance_max"
			unit="m" ucd="stat.fourier;pos;stat.max"
			utype="Char.UVAxis.Coverage.Bounds.Limits.LoLim"
			description="Maximal distance in uv plane">
			<property name="std">1</property>
		</column>
		<column name="uv_distribution_ecc"
			unit="" ucd="stat.fourier;pos"
			utype="Char.UVAxis.Coverage.Bounds.Eccentricity"
			description="Eccentricity of uv distribution">
			<property name="std">1</property>
		</column>
		<column name="uv_distribution_fill"
			unit="" ucd="stat.fourier;pos;arith.ratio"
			utype="Char.UVAxis.Coverage.Bounds.FillingFactor"
			description="Filling factor of uv distribution">
			<property name="std">1</property>
		</column>
		<column name="instrument_ant_number"
			unit="" ucd="meta.number;instr.param"
			utype="Provenance.ObsConfig.Instrument.Array.AntNumber"
			description="Number of antennas in array">
			<property name="std">1</property>
		</column>
		<column name="instrument_ant_min_dist"
			unit="m" ucd="instr.baseline;stat.min"
			utype="Provenance.ObsConfig.Instrument.Array.MinDist"
			description="Minimum distance between antennas in array">
			<property name="std">1</property>
		</column>
		<column name="instrument_ant_max_dist"
			unit="m" ucd="instr.baseline;stat.max"
			utype="Provenance.ObsConfig.Instrument.Array.MaxDist"
			description="Maximum distance between antennas in array">
			<property name="std">1</property>
		</column>
		<column name="instrument_ant_diameter"
			unit="m" ucd="instr.param"
			utype="Provenance.ObsConfig.Instrument.Array.Diameter"
			description="Diameter of telecope or antennas in array">
			<property name="std">1</property>
		</column>
		<column name="instrument_feed"
			unit="" ucd="instr.param"
			utype="Provenance.ObsConfig.Instrument.Feed"
			description="Number of feeds">
			<property name="std">1</property>
		</column>
		<column name="scan_mode"
			unit="" ucd="instr.param"
			utype="Provenance.Observation.sky_scan_mode"
			description="Scan mode (on-off,  raster map, on-the-fly map,...) ">
			<property name="std">1</property>
		</column>
		<column name="tracking_mode"
			unit="" ucd="instr.param"
			utype="Provenance.Observation.tracking_mode"
			description="Targeted, alt-azimuth, wobble, ...) ">
			<property name="std">1</property>
		</column>
	</STREAM>

	<!-- TODO: from here on, there's terribly much duplication
	with //obscore.  At the latest when we have a second obscore
	extension, we will have to refactor this. -->

	<table id="obs_radio" adql="True" onDisk="True" system="True">
		<meta name="utype">ivo://ivoa.net/std/obsradio#table-1.0</meta>

		<property key="forceStats">True</property>

		<primary>obs_publisher_did</primary>
		<foreignKey inTable="//obscore#ObsCore"
			source="obs_publisher_did"/>
		
		<meta name="description">An IVOA-defined metadata table for
			radio measurements, with extra metadata for interferometric measurements
			("visibilities") as well as single-dish observations.  You
			will almost always want to join this table to ivoa.obscore
			(do a natural join).</meta>

		<FEED source="%#obs-radio-registration"/>
		<FEED source="obs-radio-columns"/>

		<!-- bogus view creation statetment; the view is actually created
		from the _obs_radio_sources table.  It's here only so there is something
		when initially importing things.-->

		<viewStatement>create view \qName (missing) AS (SELECT 0)</viewStatement>
	</table>

	<data id="create" updating="True" auto="False">
		<!-- the view is created from prescriptions in _obs_radio_sources -->
		<make table="obs_radio">
			<script type="postCreation" lang="python" id="create-obs-radio-view">
				try:
					from gavo import rsc
					ocTable = rsc.TableForDef(
						base.resolveCrossId("//obs-radio#_obs_radio_sources"),
						connection=table.connection)
					parts = ["(%s)"%row["sql_fragment"]
						for row in ocTable.iterQuery(ocTable.tableDef, "")]
					if parts:
						table.connection.execute(
							"DROP VIEW IF EXISTS ivoa.obs_radio")
						table.connection.execute(
							"CREATE VIEW ivoa.obs_radio AS (%s)"%(
							" UNION ALL ".join(parts)))
						rsc.TableForDef(
							base.resolveCrossId("//obs-radio#obs_radio"),
							connection=table.connection).updateMeta()
				except:
					base.ui.notifyError(
						"--------  Try   dachs imp //obs-radio recover to fix this -------")
					raise
			</script>
		</make>
	</data>

	<table id="_obs_radio_sources" onDisk="True"
			dupePolicy="overwrite" primary="table_name" system="True">
		<meta name="description">
			This table contains the SQL fragments that make up this site's
			ivoa.obs_radio view.

			Manipulate this table through gavo imp on tables that have an obs_radio
			mixin, or by dropping RDs or purging tables that are part of obscore.
		</meta>

		<column name="table_name" type="text"
			description="Name of the table that generated this fragment (usually
				through a mixin)"/>
		<column name="sql_fragment" type="text"
			description="The SQL fragment contributed from table_name"/>
		<column name="sourcerd" type="text"
			description="The RD the table was found in at input (this is
				mainly to support dachs drop -f)"/>
	</table>

	<data id="init">
		<make table="_obs_radio_sources"/>
	</data>

	<data id="recover" updating="True" recreateAfter="create" auto="False">
		<!-- removes "bad" entries from obs_radio_sources -->
		<sources items="cleanup"/>
		<nullGrammar/>
		<make table="_obs_radio_sources">
			<script name="Clean bad entries from obs-radio sources"
					type="newSource" lang="python">
				q = base.UnmanagedQuerier(table.connection)

				# first, remove all tables that don't exist any more
				for tn, in q.connection.query(
						"select table_name from ivoa._obs_radio_sources"):
					if not q.getTableType(tn):
						q.connection.execute("delete from ivoa._obs_radio_sources where"
							" table_name=%(tn)s", locals())

				# then, remove tables with bad view contributions
				for tn, statement in q.connection.query(
						"select table_name, sql_fragment from ivoa._obs_radio_sources"):
					with base.getUntrustedConn() as subconn:
						try:
							_ = list(subconn.query(statement+" LIMIT 1"))
						except base.DBError:
							q.connection.execute("delete from ivoa._obs_radio_sources where"
							" table_name=%(tn)s", locals())
			</script>
		</make>
	</data>

	<mixinDef id="publish">
		<doc>
			Mix this into a table that contains radio data to have their
			metadata show up in the obscore extension for radio data.  This
			only makes sense together with one of the obscore mixins, as
			the main metadata is kept in the obscore table.
			
			Use the mixin parameters to map whatever is in your table to obs_radio'
			columns.  As with obscore, parameter values must be SQL expressions
			evaluatable within the table mixed in.  The default is to have
			all extension columns NULL.
		</doc>
		<LOOP>
			<codeItems>
				# for now, just create parameters from the extension table's
				# columns; this probably will need work even if their
				# descriptions are fixed.
				for col in context.getById("obs_radio"):
					if col.name!="obs_publisher_did":
						yield {"name": col.name, "description": col.description}
			</codeItems>
			<events>
				<mixinPar name="\name" description="\description">NULL</mixinPar>
			</events>
		</LOOP>
		<mixinPar name="obs_publisher_did"
			description="The primary key of the dataset in ivoa.obscore.
				You almost certainly do not want to touch this"
				>obs_publisher_did</mixinPar>

		<events>
			<LOOP>
				<codeItems>
				items = []
				for col in context.getById("obs_radio"):
					items.append(r"CAST(\{name} AS {type}) AS {name}".format(
						**col.asInfoDict()))
				yield {"clause": ",\n".join(items)}
				</codeItems>
				<events>
					<property name="obs-radio-clause">\clause</property>
				</events>
			</LOOP>

			<script id="add-table-to-obs-radio-sources"
					lang="python" type="afterMeta">
				obsradioClause = table.tableDef.expand(
					table.tableDef.getProperty("obs-radio-clause"))
				from gavo import rsc
				ots = rsc.TableForDef(
					base.resolveCrossId("//obs-radio#_obs_radio_sources"),
					connection=table.connection)
				ots.addRow({"table_name": table.tableDef.getQName(),
					"sourcerd": table.tableDef.rd.sourceId,
					"sql_fragment": "SELECT %s FROM %s"%(
						obsradioClause, table.tableDef.getQName())})
			</script>
			
			<script original="//obs-radio#create-obs-radio-view"
				name="create obs_radio view"/>

			<script id="remove-table-from-obs-radio-sources"
					lang="python"
					type="beforeDrop">
				from gavo import rsc
				if table.getTableType("ivoa._obs_radio_sources"):
					table.connection.execute(
						"DELETE FROM ivoa._obs_radio_sources WHERE table_name=%(name)s",
						{"name": table.tableDef.getQName()})
					# importing this table may take a long time, and we don't want
					# to have obscore offline for so long; so, we immediately recreate
					# it.
					rsc.makeData(base.resolveCrossId("//obs-radio#create"),
						connection=table.connection)
					table.connection.commit()
			</script>

		</events>
	</mixinDef>

	<!-- we probably should do an extra coverage computation of radio only -->
	<coverage fallbackTo="__system__/obscore"/>

</resource>
