<resource resdir="__system" schema="dc">
	<meta name="creationDate">2010-10-07T13:00:00</meta>
	<meta name="description">Table structures and definitions used
		in building Simple Spectral Access (SSAP) services.</meta>
	<meta name="subject">virtual-observatories</meta>

	<STREAM id="base_columns">
		<index columns="ssa_pubDID"/>

		<!-- a table containing columns required for all SSA tables.
		
		There is only minimal CoordSys metadata here; we assume
		such information is conveyed via STC groups anyway.  If that
		should turn out to be not sufficient, we'll think again. -->

		<stc>Position ICRS [ssa_location] Size "ssa_aperture" "ssa_aperture"
			SpectralInterval "ssa_specstart" "ssa_specend"
				Spectral "ssa_specmid" Size "ssa_specext"</stc>
		
		<FEED source="//products#tablecols">
			<EDIT ref="column[accref]" utype="ssa:Access.Reference"
				ucd="meta.ref.url;meta.dataset"/>
			<EDIT ref="column[mime]" utype="ssa:Access.Format"/>
			<EDIT ref="column[accsize]" utype="ssa:Access.Size" ucd=""/>
		</FEED>
		
		<column name="ssa_dstitle" type="text" required="True"
			utype="ssa:DataID.Title" ucd="meta.title;meta.dataset"
			tablehead="Title" verbLevel="15"
			description="A compact and descriptive designation of the dataset."/>
		<column name="ssa_creatorDID" type="text"
			utype="ssa:DataID.CreatorDID" ucd="meta.id"
			tablehead="C. DID" verbLevel="15"
			description="Dataset identifier assigned by the creator"/>
		<column name="ssa_pubDID" type="text"
			utype="ssa:Curation.PublisherDID"
			ucd="meta.ref.ivoid"
			tablehead="P. DID" verbLevel="15"
			description="Dataset identifier assigned by the publisher"/>
		<column name="ssa_cdate" type="timestamp"
			utype="ssa:DataID.Date" ucd="time;meta.dataset"
			tablehead="Proc. Date" verbLevel="15"
			description="Processing/Creation date"
			xtype="timestamp"/>
		<column name="ssa_pdate" type="timestamp"
			utype="ssa:Curation.Date"
			tablehead="Pub. Date" verbLevel="15"
			description="Date last published."
			xtype="timestamp"/>
		<column name="ssa_bandpass" type="text"
			utype="ssa:DataID.Bandpass" ucd="instr.bandpass"
			tablehead="Bandpass" verbLevel="15"
			description="Bandpass (i.e., rough spectral location) of this dataset;
			this should be the most appropriate term from the vocabulary
			http://www.ivoa.net/rdf/messenger."/>
		<column name="ssa_cversion" type="text"
			utype="ssa:DataID.Version" ucd="meta.version;meta.dataset"
			tablehead="C. Version" verbLevel="15"
			description="Creator assigned version for this dataset (will be
				incremented when this particular item is changed)."/>
		<column name="ssa_targname" type="text" required="True"
			utype="ssa:Target.Name" ucd="meta.id;src"
			tablehead="Object" verbLevel="15"
			description="Common name of object observed."/>
		<column name="ssa_targclass" type="text"
			utype="ssa:Target.Class" ucd="src.class"
			tablehead="Ob. cls" verbLevel="15"
			description="Object class (star, QSO,...; use Simbad object
			classification http://simbad.u-strasbg.fr/simbad/sim-display?data=otypes
			if at all possible)"/>
		<column name="ssa_redshift"
			utype="ssa:Target.Redshift" ucd="src.redshift"
			tablehead="z" verbLevel="15"
			description="Redshift of target object"/>
		<column name="ssa_targetpos" type="spoint"
			utype="ssa:Target.pos.spoint" ucd="pos.eq;src"
			tablehead="Obj. pos" verbLevel="15"
			description="Equatorial (ICRS) position of the target object."/>
		<column name="ssa_snr"
			ucd="stat.snr"
			utype="ssa:Derived.SNR"
			tablehead="SNR" verbLevel="15"
			description="Signal-to-noise ratio estimated for this dataset"/>
		<column name="ssa_location" type="spoint"
			ucd="pos.eq"
			utype="ssa:Char.SpatialAxis.Coverage.Location.Value"
			verbLevel="5" tablehead="Location"
			description="ICRS location of aperture center" unit="deg"/>
		<column name="ssa_aperture"
			unit="deg" ucd="phys.angSize;instr.fov"
			utype="ssa:Char.SpatialAxis.Coverage.Bounds.Extent"
			verbLevel="15" tablehead="Aperture"
			description="Angular diameter of aperture"/>
		<column name="ssa_dateObs" type="double precision"
			unit="d"
			utype="ssa:Char.TimeAxis.Coverage.Location.Value" ucd="time.epoch"
			verbLevel="5" tablehead="Date Obs."
			description="Midpoint of exposure (MJD)"
			displayHint="type=humanDate"
			xtype="mjd"/>
		<column name="ssa_timeExt"
			unit="s"
			utype="ssa:Char.TimeAxis.Coverage.Bounds.Extent" ucd="time.duration"
			verbLevel="5" tablehead="Exp. Time"
			description="Exposure duration"/>
		<column name="ssa_specmid"
			unit="m" ucd="instr.bandpass"
			utype="ssa:Char.SpectralAxis.Coverage.Location.Value"
			verbLevel="15" tablehead="Mid. Band"
			description="Midpoint of region covered in this dataset"/>
		<column name="ssa_specext"
			unit="m" ucd="instr.bandwidth"
			utype="ssa:Char.SpectralAxis.Coverage.Bounds.Extent"
			verbLevel="15" tablehead="Bandwidth"
			description="Width of the spectrum"/>
		<column name="ssa_specstart"
			unit="m" ucd="em.wl;stat.min"
			utype="ssa:Char.SpectralAxis.Coverage.Bounds.Start"
			verbLevel="15" tablehead="Band start"
			description="Lower value of spectral coordinate"/>
		<column name="ssa_specend"
			unit="m" ucd="em.wl;stat.max"
			utype="ssa:Char.SpectralAxis.Coverage.Bounds.Stop"
			verbLevel="15" tablehead="Band end"
			description="Upper value of spectral coordinate"/>
	</STREAM>

	<!-- The SSA metadata is huge, and many arrangements are conceivable.
	To come up with some generally useful interface definitions, I'll
	first define a table for a "homogeneous" data collection, the ssahcd
	case.  There's also a core for this. -->
	
	<STREAM id="hcd_fields">
		<!-- the SSA (HCD) fields for an instance table -->
		<FEED source="//ssap#base_columns"/>
		<column name="ssa_length" type="integer"
			utype="ssa:Dataset.Length" tablehead="Length"
			verbLevel="5"
			description="Number of points in the spectrum">
			<values nullLiteral="-1"/>
		</column>
	</STREAM>

	<STREAM id="ssa_allpars">
		<doc>Fixed parameters of all SSA tables</doc>
		<param name="ssa_model" type="text" required="True"
			utype="ssa:Dataset.DataModel"
			tablehead="Model"
			description="Data model name and version">Spectrum-1.0</param>
		<param name="ssa_csysName" type="text" required="True"
			utype="ssa:CoordSys.SpaceFrame.Name"
			tablehead="Sys" verbLevel="15"
			description="System RA and Dec are given in"
			>ICRS</param>
		<param name="ssa_timeSI" type="text"
			utype="ssa:Dataset.TimeSI"
			tablehead="[Time]"
			description="Time conversion factor in Osuna-Salgado convention."
			>\timeSI</param>

		<param name="ssa_spectralSI" type="text"
			utype="ssa:Dataset.SpectralSI"
			tablehead="[Spectral]"
			description="Spectral conversion factor in Osuna-Salgado convention"
			>\spectralSI</param>
		<param name="ssa_spectralucd" type="text" required="True"
			utype="ssa:Char.SpectralAxis.Ucd" ucd="meta.ucd"
			tablehead="UCD(spectral)" verbLevel="15"
			description="UCD of the spectral column in the spectra served;
				when you have wavelengths,
				use em.wl for vacuum wavelengths, em.wl;obs.atmos for
				air wavelengths.">\spectralUCD</param>
		<param name="ssa_spectralunit" type="text" required="True"
			utype="ssa:Char.SpectralAxis.Unit" ucd="meta.unit"
			tablehead="unit(spectral)" verbLevel="15"
			description="Unit of the spectral column">\magicEmpty{\spectralUnit}</param>

		<param name="ssa_fluxSI" type="text"
			utype="ssa:Dataset.FluxSI"
			tablehead="[Flux]"
			description="Flux/magnitude conversion factor in Osuna-Salgado convention"
			>\fluxSI</param>
		<param name="ssa_fluxucd" type="text" required="True"
			utype="ssa:Char.FluxAxis.Ucd" ucd="meta.ucd"
			tablehead="UCD(flux)" verbLevel="15"
			description="UCD of the flux column">\magicEmpty{\fluxUCD}</param>
		<param name="ssa_fluxunit" type="text" required="True"
			utype="ssa:Char.FluxAxis.Unit" ucd="meta.unit"
			tablehead="unit(flux)" verbLevel="15"
			description="Unit of the flux column">\magicEmpty{\fluxUnit}</param>
	</STREAM>

	<STREAM id="hcd_outpars">
		<doc>The parameters table for an SSA (HCD) result.  The definition
		of the homogeneous in HCD is that all these parameters are
		constant for all datasets within a table ("collection").

		These params are supposed to be filled using mixin parameters
		(or stream parameters, but the available parameters are documented
		in the hcd mixin).  Some params are hardcoded to NULL right now;
		they can easily be added to hcd's parameters if someone needs them.

		ssa_model and ssa_dstype cannot be changed right now.  Changing
		them would probably not make much sense since they reflect
		what's in this RD.</doc>

		<LFEED source="ssa_allpars"/>
		<param name="ssa_dstype" type="text"
			utype="ssa:Dataset.Type"
			tablehead="Data type"
			description="Type of data (spectrum, time series, etc)"
			>Spectrum</param>
		<param name="ssa_publisher" type="text" required="True"
			utype="ssa:Curation.Publisher" ucd="meta.curation"
			tablehead="Publisher" verbLevel="15"
			description="Publisher of the datasets included here.">\publisher</param>
		<param name="ssa_creator" type="unicode"
			utype="ssa:DataID.Creator"
			tablehead="Creator" verbLevel="15"
			description="Creator of the datasets included here.">\creator</param>
		<param name="ssa_collection" type="text"
			utype="ssa:DataID.Collection"
			tablehead="Collection" verbLevel="15"
			description="A short handle naming the collection this spectrum
				belongs to."
			>\collection</param>
		<param name="ssa_instrument" type="text"
			utype="ssa:DataID.Instrument" ucd="meta.id;instr"
			tablehead="Instrument" verbLevel="15"
			description="Instrument or code used to produce these datasets"
			>\instrument</param>
		<param name="ssa_datasource" type="text"
			utype="ssa:DataID.DataSource"
			tablehead="Src" verbLevel="15"
			description="Method of generation for the data (one of
				survey, pointed, theory, custom, artificial)."
			>\datasource</param>
		<param name="ssa_creationtype" type="text"
			utype="ssa:DataID.CreationType"
			tablehead="Using" verbLevel="15"
			description="Process used to produce the data (archival, cutout,
			filtered, mosaic, projection, spectralExtraction, or catalogExtraction)"
			>\creationType</param>
		<param name="ssa_reference" type="text"
			utype="ssa:Curation.Reference" ucd="meta.bib.bibcode"
			tablehead="Ref." verbLevel="15"
			description="URL or bibcode of a publication describing this data."
			>\reference</param>
		<param name="ssa_fluxStatError"
			unit="\fluxUnit" utype="ssa:Char.FluxAxis.Accuracy.StatError"
			ucd="stat.error;phot.flux.density;em"
			tablehead="Err. flux" verbLevel="15"
			description="Statistical error in flux"
			><values nullLiteral="NaN"/>\statFluxError</param>
		<param name="ssa_fluxSysError"
			utype="ssa:Char.FluxAxis.Accuracy.SysError"
			unit="\fluxUnit" ucd="stat.error.sys;phot.flux.density;em"
			tablehead="Sys. Err flux" verbLevel="15"
			description="Systematic error in flux"
			><values nullLiteral="NaN"/>\sysFluxError</param>
		<param name="ssa_fluxcalib" type="text"
			utype="ssa:Char.FluxAxis.Calibration"
			tablehead="Calib Flux" verbLevel="15"
			description="Type of flux calibration (ABSOLUTE, CALIBRATED, RELATIVE,
				NORMALIZED, or UNCALIBRATED)."
			>\fluxCalibration<values id="flux_calib_values" caseless="True">
				<option>ABSOLUTE</option>
				<option>CALIBRATED</option>
				<option>RELATIVE</option>
				<option>NORMALIZED</option>
				<option>UNCALIBRATED</option>
				<option>ANY</option>
			</values></param>
		<param name="ssa_binSize"
			unit="m" ucd="em.wl;spect.binSize"
			utype="ssa:Char.SpectralAxis.Accuracy.BinSize"
			tablehead="Spect. Bin" verbLevel="15"
			description="Bin size in wavelength"/>
		<param name="ssa_spectStatError"
			unit="m" ucd="stat.error;em"
			utype="ssa:Char.SpectralAxis.Accuracy.StatError"
			tablehead="Err. Spect" verbLevel="15"
			description="Statistical error in wavelength">
			<values nullLiteral="NaN"/>\statSpectError</param>
		<param name="ssa_spectSysError"
			unit="m" ucd="stat.error.sys;em"
			utype="ssa:Char.SpectralAxis.Accuracy.SysError"
			tablehead="Sys. Err. Spect" verbLevel="15"
			description="Systematic error in wavelength">
			<values nullLiteral="NaN"/>\sysSpectError</param>
		<param name="ssa_speccalib" type="text"
			utype="ssa:Char.SpectralAxis.Calibration" ucd="meta.code.qual"
			tablehead="Calib. Spect." verbLevel="15"
			description="Type of wavelength calibration">\spectralCalibration</param>
		<param name="ssa_specres"
			unit="m" ucd="spect.resolution;em.wl"
			utype="ssa:Char.SpectralAxis.Resolution"
			tablehead="Spec. Res." verbLevel="15"
			description="Resolution (in meters of wavelength) on the spectral axis"
			><values nullLiteral="NaN"/>\spectralResolution</param>
		<param name="ssa_spaceError"
			unit="deg" ucd="stat.error;pos.eq"
			utype="ssa:Char.SpatialAxis.Accuracy.StatError"
			tablehead="Err. Spc" verbLevel="15"
			description="Statistical error in position"
			><values nullLiteral="NaN"/>\statSpaceError</param>
		<param name="ssa_spaceCalib" type="text"
			ucd="meta.code.qual"
			utype="ssa:Char.SpatialAxis.Calibration"
			tablehead="Calib. Spc" verbLevel="15"
			description="Type of calibration in spatial coordinates"/>
		<param name="ssa_spaceRes"
			unit="deg" ucd="pos.angResolution"
			utype="ssa:Char.SpatialAxis.Resolution"
			tablehead="Res. Spc" verbLevel="15"
			description="Spatial resolution of data"/>
	</STREAM>

	<STREAM id="mixc_morefields">
		<doc>
			The fields that a mixc table needs on top of what hcd has; for
			simplicity, this is everything that hcd has as a parameter.
		</doc>

		<LFEED source="ssa_allpars"/>
		<LOOP>
			<codeItems>
				defaults = {"type": "real", "unit": "__NULL__", "ucd": "__NULL__"}
				for evType, name, pars in context.getById("hcd_outpars").iterEvents():
					if evType=="start" and name=="param":
						if (pars["name"].endswith("SI") # those remain params
							or pars["name"] in ["ssa_csysName", "ssa_model"]):
							continue
						if (pars["name"] in ["ssa_spaceRes", "ssa_spaceCalib",
								"ssa_spaceError"]):
							# would anyone *ever* miss those?
							continue
						if pars["name"].endswith("unit") or pars["name"].endswith("ucd"):
							# these must be parameters since they end up in the table
							# metadata
							continue
						rec = {}
						rec.update(defaults)
						rec.update(pars)
						rec["verbLevel"] = '15'
						yield rec
			</codeItems>
			<events>
				<column name="\name" type="\type"
					utype="\utype" ucd="\ucd" unit="\unit"
					tablehead="\tablehead"
					description="\description"
					verbLevel="\verbLevel"/>
			</events>
		</LOOP>
	</STREAM>

	<STREAM id="atomicCoords">
		<doc>
			A stream for form-based service's VOTables to include simple
			RA and Dec rather than normal ssa_location.

			SSA services get that from the core and don't need this.
		</doc>
		<outputField name="location_ra"
			unit="deg" ucd="pos.eq.ra;meta.main"
			utype="ssa:Char.SpatialAxis.Coverage.Location.Value.C1"
			tablehead="RA" verbLevel="15"
			select="degrees(long(ssa_location))"
			displayHint="sf=7"/>
		<outputField name="location_dec"
			unit="deg" ucd="pos.eq.dec;meta.main"
			utype="ssa:Char.SpatialAxis.Coverage.Location.Value.C2"
			tablehead="Dec" verbLevel="15"
			select="degrees(lat(ssa_location))"
			displayHint="sf=7"/>
	</STREAM>

	<STREAM id="coreOutputAdditionals">
		<!-- Fields added to the queried table def to make the core
		output table. -->
		<outputField name="ssa_score"
				utype="ssa:Query.Score"
				tablehead="Score" verbLevel="15"
				select="0">
			<description>A measure of how closely the record matches your
				query.  Higher numbers mean better matches.</description>
		</outputField>
		<FEED source="atomicCoords"/>
	</STREAM>

	<table id="instance" onDisk="False">
		<meta name="description">A sample of SSA fields for referencing and such.
		</meta>
		<!-- think about whether we can trick refposition and timescale
		  in here -->
		<FEED source="hcd_fields"/>
		<FEED source="hcd_outpars" spectralUnit="" fluxUnit=""
			publisher="junk" creator="junk" collection="" timeSI=""
			spectralSI="" fluxSI="" instrument="junk" datasource="junk"
			creationType="junk" reference="junk" fluxUCD="junk"
			spectralUCD="junk" statFluxError="NaN" sysFluxError="NaN"
			fluxCalibration="" statSpectError="NaN" sysSpectError="NaN"
			spectralCalibration="" statSpaceError="NaN"
			spectralResolution="NaN"/>
	</table>

	<procDef type="apply" id="setMeta">
		<doc>
			Sets metadata for an SSA data collection, including its
			products definition.

			Since this is only useful with the deprecated hcd and mixc mixins,
			this should no longer be used.

			The values are left in vars, so you need to do manual copying,
			e.g., using idmaps="*", or, if you need to be more specific,
			idmaps="ssa_*".
		</doc>
		<setup>
			<par key="dstitle" late="True" description="a title for the data set
				(e.g., instrument, filter, target in some short form; must be filled
				in); ssa:DataID.Title"/>
			<par key="creatorDID" late="True" description="id given by the
				creator (leave out if not applicable); ssa:DataID.CreatorDID"
				>None</par>
			<par key="pubDID" late="True" description="Id provided by the
				publisher (i.e., you); this is an opaque string and must be given;
				ssa:Curation.PublisherDID"/>
			<par key="cdate" late="True" description="date the file was
				created (or processed; optional); this must be either a string
				in ISO format, or you need to parse to a timestamp yourself;
				ssa:DataID.Date">None</par>
			<par key="pdate" late="True" description="date the file was
				last published (in general, the default is fine); ssa:Curation.Date"
				>datetime.datetime.utcnow()</par>
			<par key="bandpass" late="True" description="bandpass (i.e., rough
				spectral location) of this dataset; ssa:DataID.Bandpass"
				>None</par>
			<par key="cversion" late="True" description="creator assigned version
				for this file (should be incremented when it is changed);
				ssa:DataID.Version">None</par>
			<par key="targname" late="True" description="common name of
				the object observed; ssa:Target.Name">None</par>
			<par key="targclass" late="True" description="object class (star,
				QSO,...); ssa:Target.Class">None</par>
			<par key="redshift" late="True" description="source redshift;
				ssa:Target.Redshift">None</par>
			<par key="snr" late="True" description="signal-to-noise ratio
				estimated for this dataset; ssa:Derived.SNR">None</par>
			<par key="alpha" late="True" description="right ascension of target
				(ICRS degrees); ssa:Char.SpatialAxis.Coverage.Location.Value.C1"
				>None</par>
			<par key="delta" late="True" description="declination of target
				(ICRS degrees); ssa:Char.SpatialAxis.Coverage.Location.Value.C2"
				>None</par>
			<par key="aperture" late="True" description="angular diameter of
				aperture (expected in degrees);
				ssa:Char.SpatialAxis.Coverage.Bounds.Extent">None</par>
			<par key="dateObs" late="True" description="observation midpoint
				(you can give a datetime, a string in iso format, a jd, or an mjd,
				the latter two being told apart by comparing against 1e6)">None</par>
			<par key="timeExt" late="True" description="exposure time
				(in seconds); ssa:Char.TimeAxis.Coverage.Bounds.Extent">None</par>
			<par key="specmid" late="True" description="(ignored; only present
				for compatibility, computed from specstart and specend)"
				>None</par>
			<par key="specext" late="True" description="(ignored; only present
				for compatibility, computed from specstart and specend)"
				>None</par>
			<par key="specstart" late="True" description="lower bound of
				wavelength interval (in meters);
				ssa:Char.SpectralAxis.Coverage.Bounds.Start">None</par>
			<par key="specend" late="True" description="upper bound of
				wavelength interval (in meters);
				ssa:Char.SpectralAxis.Coverage.Bounds.Stop">None</par>
			<par key="length" late="True" description="Number of samples
				in the spectrum; ssa:Dataset.Length">None</par>
			<code>
				copiedKWs = ['dstitle', 'creatorDID', 'pubDID', 'cdate',
					'pdate', 'bandpass', 'cversion', 'targname', 'targclass',
					'redshift', 'snr', 'aperture', 'timeExt',
					'specstart', 'specend', 'length']
			</code>
		</setup>
		<code>
			vars["ssa_dateObs"] = toMJD(dateObs)
			userPars = locals()
			for kw in copiedKWs:
				vars["ssa_"+kw] = userPars[kw]
			alpha = parseFloat(alpha)
			delta = parseFloat(delta)
			specstart = parseFloat(specstart)
			specend = parseFloat(specend)
			
			if specstart is not None and specend is not None:
				vars["ssa_specext"] = specend-specstart
				vars["ssa_specmid"] = (specend-specstart)/2

			vars["ssa_region"] = vars["ssa_location"] = None
			if alpha is not None and delta is not None:
				vars["ssa_location"] = pgsphere.SPoint.fromDegrees(alpha, delta)
				if aperture:
					vars["ssa_region"] = pgsphere.SCircle.fromDALI(
						[alpha, delta, aperture]).asPoly(6)
		</code>
	</procDef>

	<procDef type="apply" id="setMixcMeta">
		<doc>
			Sets metadata for an SSA data set from mixed sources.  This will
			only work sensibly in cooperation with setMeta

			Since //ssap#mixc is deprecated, there is no reason to use this
			in new RDs.

			As with setMeta, the values are left in vars; if you did as recommended
			with setMeta, you'll have this covered as well.
		</doc>
		<setup>
			<par key="dstype" late="True"
				description="Type of data.  The only defined value currently is
					Spectrum, but you may get away with TimeSeries; ssa:Dataset.Type"
				>"spectrum"</par>
			<par key="publisher" late="True"
				description="Publisher IVO; ssa:Curation.Publisher"
					>"Take from RD"</par>
			<par key="creator" late="True" description="Creator/Author"
				>"Take from RD"</par>
			<par key="collection" late="True" description="IOVA id of the originating
				data collection (leave empty if you don't know what this
				is about)">None</par>
			<par key="instrument" late="True" description="Instrument or code
				used to produce this dataset; ssa:DataID.Instrument"
				>"Take from RD"</par>
			<par key="datasource" late="True" alias="dataSource"
				description="Generation type
				(typically, one survey, pointed, theory, custom, artificial);
				ssa:DataID.DataSource">None</par>
			<par key="creationType" late="True" description="Process used to
				produce the data (zero or more of archival, cutout, filtered,
				mosaic, projection, spectralExtraction, catalogExtraction, concatenated
				by commas); ssa:DataID.CreationType">None</par>
			<par key="reference" late="True" description="URL or bibcode of
				a publication describing this data.">"Take from RD"</par>

			<par key="binSize" late="True" description="Bin size on the
				spectral axis in m">None</par>
			<par key="fluxStatError" late="True" description="Statistical
				error for flux in units of fluxUnit">None</par>
			<par key="spectStatError" late="True" description="Statistical
				error for the spectral coordinate in m">None</par>
			<par key="fluxSysError" late="True" description="Systematic
				error for flux in units of fluxUnit">None</par>
			<par key="spectSysError" late="True" description="Systematic
				error for the spectral coordinate in m">None</par>

			<par key="fluxCalib" late="True" description="Type of flux calibration
				(one of ABSOLUTE, RELATIVE, NORMALIZED, or UNCALIBRATED);
				ssa:Char.FluxAxis.Calibration">None</par>
			<par key="specCalib" late="True" description="Type of wavelength
				Calibration (one of ABSOLUTE, RELATIVE, NORMALIZED, or UNCALIBRATED);
				ssa:Char.SpectralAxis.Calibration">None</par>
			<par key="specres" late="True" description="Resolution on the
				spectral axis; you must give this as FWHM wavelength in meters
				here. This will default to binSize if not given;
				ssa:Char.SpectralAxis.Resolution">None</par>
		</setup>
		<code>
			inVars = locals()
			for parName, metaName in [
					("publisher", "publisher"),
					("reference", "source"),
					("instrument", "instrument")]:
				if inVars[parName]=="Take from RD":
					vars["ssa_"+parName] = base.getMetaText(
						rd, metaName, acceptSequence=False)
				else:
					vars["ssa_"+parName] = inVars[parName]
			
			for copiedName in ["dstype", "collection", "datasource",
				"creationType", "fluxCalib", "specCalib", "specres", "binSize"]:
				vars["ssa_"+copiedName.lower()] = inVars[copiedName]

			if vars["ssa_specres"] is None:
				vars["ssa_specres"] = vars["ssa_binsize"]
		</code>
	</procDef>

	<STREAM id="commonMixinParams">
		<mixinPar key="fluxUnit" description="Flux unit used by the
			spectra and in SSA char metadata.  This must be a VOUnit string (use
			a single blank if your spectrum is not calibrated)."
			>__EMPTY__</mixinPar>
		<mixinPar key="spectralUnit" description="Spectral unit used by the
			spectra (SSA char metadata always is wavelength in meters).
			This must be a VOUnit string (use a single blank if your
			spectrum is not calibrated).">__EMPTY__</mixinPar>

		<mixinPar key="timeSI" description="SI conversion factor for times
			in Osuna-Salgado convention; ssa:DataSet.TimeSI (you probably want
			to leave this empty)">__NULL__</mixinPar>
		<mixinPar key="fluxSI" description="SI conversion factor for fluxes
			in the spectrum instance (not the SSA metadata) in Osuna-Salgado
			convention; ssa:Dataset.FluxSI (you probably want to leave this
			empty)">__NULL__</mixinPar>
		<mixinPar key="spectralSI" description="SI conversion factor of frequency
			or wavelength in the spectrum instance (not the SSA metadata, they
			are all in meters); ssa:Dataset.SpectralSI (you probably want to
			leave this empty)">__NULL__</mixinPar>
		<mixinPar key="spectralUCD" description="ucd of the spectral column, like
			em.freq or em.energy; default is wavelength; ssa:Char.SpectralAxis.Ucd"
			>em.wl</mixinPar>
		<mixinPar key="fluxUCD" description="ucd of the flux column, like
			phot.count, phot.flux.density, etc.  Default is for flux over
			wavelength; ssa:Char.FluxAxis.Ucd">phot.flux.density;em.wl</mixinPar>

		<mixinPar key="refposition"
			description="Reference position for times given in this table.
			Choose from http://www.ivoa.net/rdf/refposition, leave as UNKNOWN
			if in doubt.">UNKNOWN</mixinPar>
		<mixinPar key="timescale"
			description="Timescale for times given in this table.
			Choose from http://www.ivoa.net/rdf/timescale.  If in doubt,
			leaving this at TT is probably good enough for discovery."
			>TT</mixinPar>
	</STREAM>

	<mixinDef id="hcd">
		<doc><![CDATA[
			Deprecated. use `the //ssap#view mixin`_ instead.

			This mixin is for "homogeneous" data collections, where homogeneous
			means that all values in hcd_outpars are constant for all datasets
			in the collection.  This is usually the case if they all come
			from one instrument.

			Rowmakers for tables using this mixin should use the `//ssap#setMeta`_
			proc application.

			Do not forget to call the `//products#define`_ row filter in grammars
			feeding tables mixing this in.  At the very least, you need to
			say::

				<rowfilter procDef="//products#define">
					<bind name="table">"mySchema.myTableName"</bind>
				</rowfilter>
		]]></doc>

		<FEED source="commonMixinParams"/>

		<mixinPar key="creator" description="Creator designation;
			ssa:DataID.Creator">__NULL__</mixinPar>
		<mixinPar key="publisher" description="Publisher IVO (by default
			 taken from the DC config); ssa:Curation.Publisher"
			 >\metaString{publisherID}{\metaString{publisher}}</mixinPar>
		<mixinPar key="instrument" description="Instrument or code used to produce
			these datasets; ssa:DataID.Instrument">__NULL__</mixinPar>
		<mixinPar key="datasource" alias="dataSource"
			description="Generation type (typically, one
			survey, pointed, theory, custom, artificial); ssa:DataID.DataSource"
			>__NULL__</mixinPar>
		<mixinPar key="creationType" description="Process used to
			produce the data (zero or more of archival, cutout, filtered,
			mosaic, projection, spectralExtraction, catalogExtraction);
			ssa:DataID.CreationType">__NULL__</mixinPar>
		<mixinPar key="reference" description="URL or bibcode of a
			publication describing this data; ssa:Curation.Reference"
			>__NULL__</mixinPar>
		<mixinPar key="statFluxError" description="Statistical error in
			flux; ssa:Char.FluxAxis.Accuracy.StatError"
			>__NULL__</mixinPar>
		<mixinPar key="sysFluxError" description="Systematic error in flux;
			ssa:Char.FluxAxis.Accuracy.SysError"
			>__NULL__</mixinPar>
		<mixinPar key="fluxCalibration" description="Type of flux calibration
			(one of ABSOLUTE, RELATIVE, NORMALIZED, or UNCALIBRATED);
			ssa:Char.FluxAxis.Calibration"/>
		<mixinPar key="statSpectError"
			description="Statistical error in wavelength (units of specralSI);
			ssa:Char.SpectralAxis.Accuracy.StatError">__NULL__</mixinPar>
		<mixinPar key="sysSpectError" description="Systematic error in wavelength
			(in m); ssa:Char.SpectralAxis.Accuracy.SysError"
			>__NULL__</mixinPar>
		<mixinPar key="spectralCalibration" description="Type of wavelength
			Calibration (one of ABSOLUTE, RELATIVE, NORMALIZED, or UNCALIBRATED);
			ssa:Char.SpectralAxis.Calibration">__NULL__</mixinPar>
		<mixinPar key="statSpaceError" description="Statistical error in position
			in degrees;
			ssa:Char.SpatialAxis.Accuracy.StatError"
			>__NULL__</mixinPar>
		<mixinPar key="collection" description="ivo id of the originating
			collection; ssa:DataID.Collection">__NULL__</mixinPar>
		<mixinPar key="spectralResolution"
			description="Resolution on the spectral axis; you must give this
			as FWHM wavelength in meters here. Approximate as necessary;
			ssa:Char.SpectralAxis.Resolution">NaN</mixinPar>

		<FEED source="//products#hackProductsData"/>
		<events>
			<dm>
				(votable:Coords) {
					time: {
						frame: {
							timescale: \timescale
							refPosition: \refposition
							time0: 2400000.5
						}
						location: @ssa_dateObs
					}
				}
			</dm>

			<index columns="ssa_pubDID"/>
			<index columns="ssa_targname"/>
			<index columns="ssa_location" method="GIST"/>
			<FEED source="//products#tableRules"/>
			<LFEED source="//ssap#hcd_fields"/>
			<LFEED source="//ssap#hcd_outpars"/>
		</events>

		<processEarly>
			<code>
				# these secret attributes are used by the sdm-instance
				substrate.from_mixin_spectral_unit = mixinPars["spectralUnit"]
				substrate.from_mixin_spectral_ucd = mixinPars["spectralUCD"]
				substrate.from_mixin_flux_unit = mixinPars["fluxUnit"]
				substrate.from_mixin_flux_ucd = mixinPars["fluxUCD"]
			</code>
		</processEarly>
	</mixinDef>


	<mixinDef id="mixc">
		<doc><![CDATA[
			Deprecated. use `the //ssap#view mixin`_ instead.
			
			This mixin provides the columns and params for a common SSA service.

			Rowmakers for tables using this mixin should use the `//ssap#setMeta`_
			and the `//ssap#setMixcMeta`_ proc applications.

			There are some limitations to the variability; in particular, all
			spectra must have the same types of axes (i.e., frequency, wavelength,
			or energy) with identical units.  If you don't have that,
			either leave the respective metadata empty or homogenize it
			before ingestion.

			Do not forget to call the `//products#define`_ row filter in grammars
			feeding tables mixing this in.  At the very least, you need to
			say::

				<rowfilter procDef="//products#define">
					<bind name="table">"schema.table"</bind>
				</rowfilter>
		]]></doc>
		
		<FEED source="commonMixinParams"/>

		<FEED source="//products#hackProductsData"/>
		<events>
			<dm>
				(votable:Coords) {
					time: {
						frame: {
							timescale: \timescale
							refPosition: \refposition
							time0: 2400000.5
						}
						location: @ssa_dateObs
					}
				}
			</dm>

			<index columns="ssa_pubDID"/>
			<index columns="ssa_targname"/>
			<index columns="ssa_location" method="GIST"/>
			<FEED source="//products#tableRules"/>
			<LFEED source="//ssap#hcd_fields"/>
			<LFEED source="//ssap#mixc_morefields"/>
		</events>

		<processEarly>
			<code>
				# these secret attributes are used by the sdm-instance
				substrate.from_mixin_spectral_unit = mixinPars["spectralUnit"]
				substrate.from_mixin_spectral_ucd = mixinPars["spectralUCD"]
				substrate.from_mixin_flux_unit = mixinPars["fluxUnit"]
				substrate.from_mixin_flux_ucd = mixinPars["fluxUCD"]
			</code>
		</processEarly>
	</mixinDef>


	<mixinDef id="view">
		<doc><![CDATA[
			This mixin produces an SSA-ready relation as a view.

			The idea is that you import your spectra into a table suitable for your
			particular data collection (but mixing in `//products#define`_).  You
			then fill the columns for an SSA response giving in each mixin parameter
			here either with a column reference (as a simple column name) or with a
			SQL literals (put strings into single quotes – sourcetable is the
			exception here).  Save typing by having the final column names
			in the source table and using the ``copiedcolumns`` mixin par.

			If you have positions for your spectra, you probably want to also mix in
			`the //ssap#plainlocation mixin`_ in the original table in order to have
			indexed positions in a way suitable for SSA queries.

			In general, you will have to generate indices on the source table;
			postgres doesn't support indices on views.  If you can't use the
			plainlocation mixin, please not that the the SSA engine expects
			spoints as the location (and these would be indexed like
			<index columns="loc_col" method="GIST"/>).

			The mixin will automatically create an index over whatever you
			give for ssa_pubDID (if you give something).
		]]></doc>

		<mixinPar key="sourcetable" description="Reference to the table
			to build the view upon (i.e., the value of its id attribute).
			This mixin can only build onto a single table.  To make an
			SSA table based on a join of multiple underlying table, define
			an intermediate table with a viewStatement of its own and use
			that as sourcetable."/>
		<mixinPar key="materialize" description="Set to True to
			make this a materialized view.  For smallish tables, that
			is a reasonable thing to do that generally simplifies the handling
			a bit.  On the other hand, the view will no longer follow the
			underlying tables.">False</mixinPar>
		<mixinPar key="copiedcolumns" description="rowmaker/idmaps-like list of
			strings with column names or shell patterns to copy from sourcetable.
			These columns will automatically be taken over in the view.
			SSA columns taken over in this way *cannot* be overridden in
			mixin parameters; the mandatory mixin parameters ssa_spectralunit
			and ssa_fluxcalib have to be given (with ignored values) even
			if they are in copiedcolumns.">__EMPTY__</mixinPar>

		<mixinPar key="refposition"
			description="Reference position for times given in this table.
			Choose from http://www.ivoa.net/rdf/refposition, leave as UNKNOWN
			if in doubt.">UNKNOWN</mixinPar>
		<mixinPar key="timescale"
			description="Timescale for times given in this table.
			Choose from http://www.ivoa.net/rdf/timescale.  If in doubt,
			leaving this at TT is probably good enough for discovery."
			>TT</mixinPar>

		<mixinPar key="ssa_region" description="A region covered by the
			observation for this spectrum; this will be ``ssa_region`` when
			you mix //ssap#simpleCoverage into the original table.  You'll
			probably want to leave this at the default otherwise."
			>not given</mixinPar>
		<mixinPar key="customcode" description="A SQL fragment with extra
			column definitions to go into the view.  This must start with
			a comma (because it's going to be appended to the select clause)."
			>__EMPTY__</mixinPar>
		<mixinPar key="whereclause" description="A SQL fragment that
			fits after the source table (typically, a WHERE clause).  Use
			this if you only want a subset of the original table to be
			visible through SSAP.">__EMPTY__</mixinPar>
		<LOOP>
			<codeItems>
				VIEWPAR_DEFAULTS = {
					# maps view column names to defaults for the mixin
					"accref": "accref",
					"owner": "owner",
					"embargo": "embargo",
					"mime": "mime",
					"accsize": "accsize",
					"ssa_aperture": "NULL",
					"ssa_bandpass": "NULL",
					"ssa_collection": "NULL",
					"ssa_creationtype":
						"\\sqlquote{\\metaString{ssap.creationType}{unknown}}",
					"ssa_csysName": "'ICRS'",
					"ssa_datasource":
						"\\sqlquote{\\metaString{ssap.dataSource}{unknown}}",
					"ssa_dateObs": "NULL",
					"ssa_dstitle": "NULL",
					"ssa_dstype": "'spectrum'",
					"ssa_fluxcalib": "",
					"ssa_fluxucd": "'phot.flux.density;em.wl'",
					"ssa_instrument": "\\sqlquote{\\metaString{instrument}{unknown}}",
					"ssa_length": "NULL",
					"ssa_location": "NULL",
					"ssa_model": "'Spectrum-1.0'",
					"ssa_pdate": "\\sqlquote{\\today}",
					"ssa_publisher": "\\sqlquote{\\metaString{publisher}{unknown}}",
					"ssa_pubDID": "\\sql_standardPubDID",
					"ssa_reference": "\\sqlquote{\\metaString{source}{unknown}}",
					"ssa_specres": "NULL",
					"ssa_spectralucd": "'em.wl;obs.atmos'",
					"ssa_spectralunit": "",
					"ssa_specmid": "(ssa_specend+ssa_specstart)/2",
					"ssa_specext": "(ssa_specend-ssa_specstart)",
				}

				DESCRIPTION_OVERRIDES = {
					"ssa_binSize": "Size of the typical spectral bin in meters"
						" of wavelength.",
					"ssa_cdate": "Dataset creation date as a datetime",
					"ssa_pdate": "Date of (last) publication as a datetime",
					"ssa_dataObs": "MJD of the midpoint of the exposure",
					"ssa_fluxSI": "It's safe to ignore this.",
					"ssa_timeSI": "It's safe to ignore this.",
					"ssa_spectralSI": "It's safe to ignore this.",
					"ssa_fluxunit": "Unit of the flux column in the dataset.  This"
						" is also the unit for the ssa_flux*Error columns (and thus"
						" better had not change between rows).",
					"ssa_spectralunit": "Unit of the flux column in the dataset"
						" (e.g., 'Angstrom').  This is also the unit for the"
						" ssa_spectral*Error columns (and thus better had not"
						" change between rows).",
					"ssa_location": "You probably don't want to manually"
						" handle this.  Use `the //ssap#plainlocation mixin`_"
						" on your metadata table instead and write ssa_location here.",
					"ssa_region": "You probably don't want to manually"
						" handle this.  Use  `the /ssap#simpleCoverage mixin`_"
						" on your metadata table instead.",
					"ssa_targetpos": "Position of the intended target of the"
						" observation.  You don't usually need to give this."
						" if you do, you probably want to write"
						" pgsphere.SPoint.fromDegrees(targ_ra, targ_dec)",
				}

				from gavo.protocols import ssap
				for c in ssap.iterViewColumns(context):
					yield {"name": c.name,
						"description": DESCRIPTION_OVERRIDES.get(c.name, c.description),
						"default": VIEWPAR_DEFAULTS.get(c.name, "NULL")}
			</codeItems>
			<events>
				<mixinPar key="\name" description="\description">\default</mixinPar>
			</events>
		</LOOP>

		<processEarly>
			<setup imports="collections, fnmatch, gavo.adql, gavo.rscdef, weakref,
					gavo.rscdef.column.makeOptionsFromDist">
				<code>
					valueExpression = adql.getRawGrammar()[0]["valueExpression"]

					def getCopiedColumns(copiedColumnsSpec, baseTable):
						"""returns a sequence for columns in baseTable matching
						copiedColumnsSpec.

						copiedColumnsSpec has comma-separated shell patterns as in
						rowmaker/@idmaps.

						Refactoring this and rscdef.rmkdef.RowmakerDef._resolveIdmaps
						is probably not worth it.
						"""
						baseNames = [c.key for c in baseTable]
						matchingNames = []
						for pat in [s.strip() for s in copiedColumnsSpec.split(",")]:
							matchingNames.extend(fnmatch.filter(baseNames, pat))
						
						# if a column has matched multiple times, it will
						# be returned multiple times; since later occurrences
						# overwrite earlier ones in tables, that shouldn't hurt.
						return [baseTable.getColumnByName(n)
							for n in matchingNames]

					# Change product table column utypes; this is always universally
					# necessary because these columns typically come from sourcetable
					# and there from //products#tablecols.  And where's not necessary
					# it certainly doesn't hurt either.
					UTYPE_OVERRIDES = {
						"accref": ("ssa:Access.Reference", "meta.ref.url;meta.dataset"),
						"mime": ("ssa:Access.Format", "meta.code.mime"),
						"accsize": ("ssa:Access.Size", "")}

					def applyOverrides(col):
						if col.name in UTYPE_OVERRIDES:
							utype, ucd = UTYPE_OVERRIDES[col.name]
							return col.change(utype=utype, ucd=ucd)
						return col

					def nullToEmpty(val):
						return "" if val.lower()=="null" else val

					def makeOptionsForConstant(sqlLit, col):
						"""sets col's values to sqlLit's string value if it is
						a string literal.

						This is mainly cosmetics for VOSI tablesets, where we don't
						want *calib columns to oddly stick out with a lot of
						irrelevant options.
						"""
						# better say nothing than bleed out non-expanded
						# macros
						if '\\\\' in sqlLit:
							return

						if col.type=="text" and utils.isSQLTextLiteral(sqlLit):
							if col.values is None:
								col.values = base.makeStruct(rscdef.Values, parent_=col)
							col.values.options = makeOptionsFromDist({
								sqlLit.strip()[1:-1].replace("''", "'"): 1})
							col.values.actual = True
				</code>
			</setup>
			<code>
				from gavo.protocols import ssap
				substrate.viewPieces = collections.OrderedDict()

				for c in ssap.iterViewColumns(context):
					substrate.feedObject("column", applyOverrides(c))

					val = mixinPars[c.name]
					if '\\\\' not in val:
						# this is *not* a security feature (if you can run this
						# mixin, you can run SQL through the viewStatement anyway).
						# This is just so you get get perhaps more useful error messages
						# in some cases.
						try:
							_ = valueExpression.parseString(val, parseAll=True)
						except base.ParseException:
							raise base.StructureError("Mixin par %s=%s does not match SQL"
								" valueExpression."%(c.name, val),
								hint="ssap#view parameters are in general put literally"
									" into a view declaration and hence must be valid SQL."
									"  What you put in here is not recognised by DaCHS'"
									" ADQL parser.  If you are sure postgres will swallow"
									" it, prefix it with backslash-+ to make DaCHS accept it, too.")
					substrate.viewPieces[c.name] = "(%s)::%s AS %s"%(
						val, base.sqltypeToPG(c.type), c.name)

					makeOptionsForConstant(val, c)
					
				# ssa_region is a local addition (yet) and can't be taken
				# from ssap#instance.
				if mixinPars["ssa_region"]!="not given":
					substrate.feedObject("column",
						MS(rscdef.Column, name="ssa_region",
							type="spoly", tablehead="Coverage",
							description="Rough coverage based on location and aperture",
							verbLevel=30))
					substrate.viewPieces["ssa_region"] = (
						"(%s)::spoly AS ssa_region"%mixinPars["ssa_region"])

				# Copy over columns from the source table (these will overwrite
				# stuff from ssap#instance)
				sourceTable = base.resolveId(
					context, mixinPars["sourcetable"], forceType=rscdef.TableDef)
				for c in getCopiedColumns(mixinPars["copiedcolumns"], sourceTable):
					substrate.feedObject("column", applyOverrides(c))
					substrate.viewPieces[c.name] = "(%s)::%s AS %s"%(
						c.name, base.sqltypeToPG(c.type), c.name)

				# the source table almost always needs an index we can use
				# for our pubdids, so just create it.
				if mixinPars["ssa_pubDID"]:
					sourceTable.feedObject("index",
						base.makeStruct(rscdef.DBIndex,
							columns=["ssa_pubDID"],
							content_="(%s)"%mixinPars["ssa_pubDID"],
							name="obscore_%s_did_index"%substrate.id))
				
				# these secret attributes are used by the sdm-instance
				substrate.from_mixin_spectral_unit = nullToEmpty(
					mixinPars["ssa_spectralunit"]).strip("'")
				substrate.from_mixin_spectral_ucd = nullToEmpty(
					mixinPars["ssa_spectralucd"]).strip("'")
				substrate.from_mixin_flux_unit = nullToEmpty(
					mixinPars["ssa_fluxunit"]).strip("'")
				substrate.from_mixin_flux_ucd = nullToEmpty(
					mixinPars["ssa_fluxucd"]).strip("'")
			</code>
		</processEarly>

		<processLate>
			<code>
				# Now override view definitions for columns the substrate
				# got from the source table.
				sourceTable = base.resolveId(
					context, mixinPars["sourcetable"])

				for col in substrate.columns:
					if col.name in sourceTable:
						substrate.viewPieces[col.name] = "%s::%s"%(
							col.name, base.sqltypeToPG(col.type))
			
				if base.parseBooleanLiteral(mixinPars["materialize"]):
					materialization = "MATERIALIZED"
				else:
					materialization = ""

				substrate.viewStatement = substrate.viewStatement.replace(
					"__VIEW_DEF_LINES__", ",\n".join(substrate.viewPieces.values())
					).replace(
					"__SOURCETABLE_NAME__", sourceTable.getQName()
					).replace(
					"__MATERIALIZATION__", materialization
					).replace(
					"__COL_NAMES__", ", ".join(c.name for c in substrate))
			</code>
		</processLate>

		<events>
			<onDisk>True</onDisk>
			<dm>
				(votable:Coords) {
					time: {
						frame: {
							timescale: \timescale
							refPosition: \refposition
							time0: 2400000.5
						}
						location: @ssa_dateObs
					}
				}
			</dm>

			<viewStatement>
				CREATE __MATERIALIZATION__ VIEW \\curtable AS (
					SELECT __COL_NAMES__ FROM (
						SELECT
							__VIEW_DEF_LINES__ \customcode
						FROM __SOURCETABLE_NAME__
						\whereclause) AS internal_ssa_view)
			</viewStatement>
		</events>

		<lateEvents passivate="True">
			<FEED
				source="//procs#declare-indexes-from"
				sourceTables="\sourcetable"/>
		</lateEvents>
	</mixinDef>

	<procDef id="parablePQLPar" type="phraseMaker">
		<doc>A procDef for condDescs that may match against table
		params *or* table columns, depending on where consCol is found.
		</doc>
		<setup>
			<par name="consCol" description="Name of the database column
				constrained by the input value."/>
			<par name="parClass" description="Identifier of a PQLPar
				(sub)class that is used to parse the incoming value."/>
			<code>
				from gavo import rscdef
			</code>
		</setup>
		<code>
			key = inputKeys[0].name
			val = inPars.get(key, None)
			if val is None or val.lower()=="any":
				return
			parsed = parClass.fromLiteral(val, key)

			valueSource = core.queriedTable.getByName(consCol)
			if isinstance(valueSource, rscdef.Param):
				if (valueSource.value is not None
						and not parsed.covers(valueSource.value)):
					yield '1!=1'
			else:
				yield parsed.getSQL(consCol, outPars)
		</code>
	</procDef>

	<STREAM id="hcd_condDescs">
		<doc>
			This stream defines the condDescs for an SSA service based on
			one of the mixins defined here.
		</doc>
		<condDesc id="coneCond" combining="True" joiner="AND">
			<!-- condCond is combining to let the client specify SIZE but
			not POS (as splat does); pql#coneParameter can handle that. -->
			<inputKey name="POS" type="text" description="ICRS position of target
				object" unit="deg" std="True" multiplicity="single"
				utype="ssa:Char.SpatialAxis.Coverage.Location.Value"/>
			<inputKey name="SIZE" description="Size of the region of
				interest around POS" std="True" multiplicity="single"
				unit="deg"
				utype="ssa:Char.SpatialAxis.Coverage.Bounds.Extent"/>
			<phraseMaker procDef="//pql#coneParameter">
				<bind name="posCol">"ssa_location"</bind>
			</phraseMaker>
		</condDesc>

		<condDesc id="bandCond">
			<inputKey name="BAND" type="text" description="Wavelength (range)
				of interest (or symbolic bandpass names)" unit="m"
				multiplicity="single"
				std="True" utype="ssa:DataId.Bandpass"/>
			<phraseMaker>
				<code>
					key = inputKeys[0].name
					lit = inPars.get(key, None)
					if lit is None:
						return
					try:
						ranges = pql.PQLFloatPar.fromLiteral(lit, key)
						if ranges is None: # null string
							return
						yield ranges.getSQLForInterval(
							"ssa_specstart", "ssa_specend", outPars)
					except base.LiteralParseError:
						# As float ranges, things didn't work out.  Try band names ("V")
						# and bail out if unsuccessful.
						yield pql.PQLPar.fromLiteral(lit, key).getSQL(
							"ssa_bandpass", outPars)
				</code>
			</phraseMaker>
		</condDesc>

		<condDesc id="timeCond">
			<inputKey original="//ssap#instance.ssa_dateObs" name="TIME" unit=""
				type="text" std="True" multiplicity="single"/>
			<phraseMaker procDef="//pql#dateParameter">
				<bind name="consCol">"ssa_dateObs"</bind>
				<bind name="consColKind">"mjd"</bind>
			</phraseMaker>
		</condDesc>

		<condDesc id="formatCond">
			<inputKey original="//ssap#instance.mime" name="FORMAT" type="text"
				std="True" multiplicity="single"/>
			<phraseMaker id="makeFormatPhrase">
				<setup>
					<par name="compliantFormats">frozenset([
						"application/x-votable+xml"])</par>
					<par name="nativeFormats">frozenset([
						"application/fits", "text/csv", "text/plain", "image/fits"])</par>
					<par name="consCol">"mime"</par>
				</setup>
				<code>
					val = inPars.get("FORMAT", None)
					if val is None:
						return
					if val=="" or val=="ALL":  # no constraint
						return

					sel = pql.PQLStringPar.fromLiteral(
						val.lower(), "FORMAT").getValuesAsSet()

					if "all" in sel:
						return  # No constraints

					if "compliant" in sel:
						yield "%s IN %%(%s)s"%(consCol, base.getSQLKey(consCol,
							compliantFormats, outPars))
						sel.remove("compliant")

					if "native" in sel:
						yield "%s IN %%(%s)s"%(consCol, base.getSQLKey(consCol,
							nativeFormats, outPars))
						sel.remove("native")

					if "graphic" in sel:
						yield "%s LIKE 'image/%%'"%(consCol)
						sel.remove("graphic")

					if "votable" in sel:
						yield "%s = 'application/x-votable+xml'"%consCol
						sel.remove("votable")

					if "fits" in sel:
						yield "%s = 'application/fits'"%consCol
						sel.remove("fits")

					if "xml" in sel:
						yield "1=0"  # whatever would *that* be?
						sel.remove("xml")
					
					if sel:
						yield "%s IN %%(%s)s"%(consCol, base.getSQLKey(consCol,
							sel, outPars))
				</code>
			</phraseMaker>
		</condDesc>


		<!--
			The following ssa keys cannot be generically supported since
			no SSA model column corresponds to them:
			VARAMPL, TIMERES -->
		<LOOP>
			<csvItems>
				keyName,      matchCol,      procDef
				APERTURE,     ssa_aperture,  //pql#floatParameter
				SNR,          ssa_snr,       //pql#floatParameter
				REDSHIFT,     ssa_redshift,  //pql#floatParameter
				TARGETNAME,   ssa_targname,  //pql#irStringParameter
				TARGETCLASS,  ssa_targclass, //pql#stringParameter
				MTIME,        ssa_pdate,     //pql#dateParameter
			</csvItems>
			<events>
				<condDesc id="\keyName\+_cond">
					<inputKey original="//ssap#instance.\matchCol" name="\keyName"
						type="text" xtype="" std="True" multiplicity="single"/>
					<phraseMaker procDef="\procDef">
						<bind name="consCol">"\matchCol"</bind>
					</phraseMaker>
				</condDesc>
			</events>
		</LOOP>

		<!-- the following keys may need to be compared against PARAMs,
		hence the special handling -->
		<LOOP>
			<csvItems>
				keyName,      matchCol,        parClass
				FLUXCALIB,    ssa_fluxcalib,   pql.PQLCaselessPar
				SPATRES,      ssa_spaceRes,    pql.PQLFloatPar
				WAVECALIB,    ssa_speccalib,   pql.PQLCaselessPar
				COLLECTION,   ssa_collection,  pql.PQLPar
			</csvItems>
			<events>
				<condDesc id="\keyName\+_cond">
					<inputKey original="//ssap#instance.\matchCol" name="\keyName"
						type="text" std="True" multiplicity="single"/>
					<phraseMaker procDef="//ssap#parablePQLPar">
						<bind name="consCol">"\matchCol"</bind>
						<bind name="parClass">\parClass</bind>
					</phraseMaker>
				</condDesc>
			</events>
		</LOOP>

		<!-- SPECRP is an extra madness because it queries against
		λ/Δλm which isn't even in the SSA table.  Oh, boy. -->

		<condDesc id="SPECRP_cond">
			<inputKey name="SPECRP" type="real" std="True"
				description="Lower limit to spectral resolution power
					(λ/Δλ) as a single number."
				multiplicity="single"/>
			<phraseMaker>
				<setup>
					<code>
						from gavo import rscdef
					</code>
				</setup>
				<code> <![CDATA[
					valueSource = core.queriedTable.getByName("ssa_specres")
					if isinstance(valueSource, rscdef.Param):
						if valueSource.value is None:
							raise base.ValidationError("Unknown spectral resolution"
								" in %s"%core.queriedTable.getQName(), "SPECRP")
						else:
							yield '%%(%s)s < ssa_specend/%s'%(
								base.getSQLKey("specrp", inPars["SPECRP"], outPars),
								valueSource.value)

					elif isinstance(valueSource, rscdef.Column):
						yield '%%(%s)s < ssa_specend/ssa_specres'%(
							base.getSQLKey("specrp", inPars["SPECRP"], outPars))

					else:
							raise base.ValidationError("Table %s has insufficient"
								" information to query against spectral resolution"
								" power."%core.queriedTable.getQName(), "SPECRP")
				]]></code>
			</phraseMaker>
		</condDesc>

		<LOOP>
			<csvItems>
				keyName,      matchCol
				PUBDID,       ssa_pubDID
				CREATORDID,   ssa_creatorDID
			</csvItems>
			<events>
				<condDesc>
					<inputKey original="//ssap#instance.\matchCol" name="\keyName"
						std="True" multiplicity="single"/>
					<phraseMaker>
						<code>
							yield "\matchCol=%%(%s)s"%base.getSQLKey("\keyName",
								inPars["\keyName"], outPars)
						</code>
					</phraseMaker>
				</condDesc>
			</events>
		</LOOP>

		<!-- WILDTARGET and WILDTARGETCASE are funky special cases. -->
		<LOOP>
			<csvItems>
				keyName,        parClass,                 addDesc
				WILDTARGET,     PQLNocaseShellPatternPar, (case insensitive)
				WILDTARGETCASE, PQLShellPatternPar,       (case sensitive)
			</csvItems>
			<events>
				<condDesc id="\keyName\+_cond">
					<inputKey name="\keyName" type="text" tablehead="Name Pattern"
						description="Shell pattern of target observed \addDesc"
						multiplicity="single"/>
					<phraseMaker>
						<code>
							parsed = pql.\parClass.fromLiteral(
								inPars.get("\keyName"), "\keyName")
							if parsed is not None:
								yield parsed.getSQL("\keyName", outPars)
						</code>
					</phraseMaker>
				</condDesc>
			</events>
		</LOOP>

		<condDesc combining="True" joiner="AND">
			<!-- meta keys not (directly) entering the query -->
			<inputKey name="REQUEST" type="text" tablehead="Request type"
				description='This is queryData for the default operation; some
					services support getData as well' std="True"
				multiplicity="single"
				required="True">
				<property key="defaultForForm">queryData</property>
			</inputKey>
			<inputKey name="TOP" type="integer" tablehead="#Best"
				multiplicity="single"
				description='Only return the TOP "best" records (on this service,
					this usually is equivalent to MAXREC, so no ranking is implied).'
					std="True"/>
			<inputKey name="COMPRESS" type="boolean" tablehead="Compress?"
				multiplicity="single"
				description="Return compressed results?"
				std="True">True</inputKey>
			<inputKey name="RUNID" type="text" tablehead="Run id"
				multiplicity="single"
				description="An identifier for a certain run.  Opaque to the service"
				std="True"/>
			<phraseMaker> <!-- done by the core code for these -->
				<code>
					if False:
						yield
				</code>
			</phraseMaker>
		</condDesc>
	</STREAM>

	<NXSTREAM id="makeSpecGroup" doc="copies over SSA fields into groups
		required by the spectral DM; enter the names of the fields,
		whitespace-separated, in the fieldnames parameter">
		<group utype="\groupUtype">
			<LOOP>
				<codeItems>
					from gavo.protocols import sdm
					srcTable = context.getById("instance")
					for name in "\fieldnames".split():
						utype = sdm.getSDM1UtypeForSSA(
							srcTable.getElementForName(name).utype)
						yield {"dest": name, "utype": utype}
				</codeItems>
				<events>
					<paramRef dest="\\dest" utype="\\utype"/>
				</events>
			</LOOP>
		</group>
	</NXSTREAM>

	<mixinDef id="sdm-instance">
		<doc><![CDATA[
			This mixin is intended for tables that get serialized into documents
			conforming to the Spectral Data Model 1, specifically to VOTables

			The input to such tables comes from ssa tables (hcd, in this case).
			Their columns (and params) are transformed into params here.

			The mixin adds two columns (you could add more if, e.g., you had
			errors depending on the spectral or flux value), spectral (wavelength
			or the like) and flux.  Their metadata is taken from the ssa fields
			where available (ssa_fluxucd as flux UCD, ssa_fluxunit etc).

			This mixin in action could look like this::

				<table id="instance" onDisk="False">
					<mixin ssaTable="spectra"
						fluxUnit="Jy"
						>//ssap#sdm-instance</mixin>
				</table>

			The mixin thus defines a gazillion of params.  This will almost
			always be filled using `//ssap#feedSSAToSDM`_ as explained in
			`SDM compliant tables`_
			]]></doc>
		
		<!-- technically the sdm-instance defines the (silly) SDM-groups
		using the translation of names to utypes as given by the
		ssa instance above, whereas the actual params and columns are
		taken from the ssaTable.  This works if the ssa table actually
		mixes in ssap#hcd; otherwise, you're on your own. -->

		<mixinPar key="ssaTable" description="The SSAP (HCD) instance table
			 to take the params from"/>
		<mixinPar key="spectralDescription" description="Description for the
			spectral column"
				>The independent variable of this spectrum (see its ucd to figure out whether it's a wavelength, frequency, or energy)</mixinPar>
		<mixinPar key="fluxDescription" description="Description
			for the flux column">The dependent variable of this spectrum (see the ucd for its physical meaning)</mixinPar>
		<mixinPar key="fluxUnitOverride" description="Force unit of
			 the spectral column (don't use this; fix your SSA table instead)"
			 >__EMPTY__</mixinPar>
		<mixinPar key="fluxUCDOverride" description="Force UCD of
			 the spectral column (don't use this; fix your SSA table instead)"
			 >__EMPTY__</mixinPar>
		<mixinPar key="spectralUnitOverride" description="Force unit of
			 the spectral column (don't use this; fix your SSA table instead)"
			 >__EMPTY__</mixinPar>
		<mixinPar key="spectralUCDOverride" description="Force UCD of the
			 spectral column (don't use this)">__EMPTY__</mixinPar>

		<events>
			<FEED source="makeSpecGroup"
				groupUtype="spec:Spectrum.Target"
				fieldnames="ssa_targname ssa_redshift ssa_targetpos ssa_targclass"/>
			<FEED source="makeSpecGroup"
				groupUtype="spec:Spectrum.Char"
				fieldnames="ssa_location ssa_aperture ssa_dateObs ssa_timeExt
					ssa_specmid ssa_specext ssa_specstart ssa_specend ssa_spectralucd
					ssa_binSize ssa_fluxSysError ssa_fluxStatError
					ssa_spectStatError ssa_spectSysError ssa_speccalib
					ssa_specres ssa_spectralunit ssa_fluxunit ssa_fluxucd"/>
			<FEED source="makeSpecGroup"
				groupUtype="spec:Spectrum.Curation"
				fieldnames="ssa_reference ssa_pubDID ssa_pdate ssa_publisher"/>
			<FEED source="makeSpecGroup"
				groupUtype="spec:Spectrum.DataID"
				fieldnames="ssa_dstitle ssa_creatorDID ssa_cdate ssa_bandpass
					ssa_cversion ssa_creator ssa_collection ssa_instrument
					ssa_datasource ssa_creationtype"/>
			<FEED source="makeSpecGroup"
				groupUtype="spec:Spectrum.Derived"
				fieldnames="ssa_snr"/>
			<FEED source="makeSpecGroup"
				groupUtype="spec:Spectrum.CoordSys"
				fieldnames="ssa_csysName"/>

			<!-- UCDs and Units filled in by processEarly -->
			<column name="spectral" type="double precision"
				utype="spec:spectrum.data.spectralaxis.value"
				description="\spectralDescription"/>
			<column name="flux" type="double precision"
				utype="spec:spectrum.data.fluxaxis.value"
				description="\fluxDescription"/>
			<group utype="spec:spectrum.data">
				<columnRef key="spectral"
					utype="spec:spectrum.data.spectralaxis.value"/>
				<columnRef key="flux" utype="spec:spectrum.data.fluxaxis.value"/>
			</group>
		</events>

		<processEarly>
			<setup>
				<code>
					from gavo import base
					from gavo import rscdef
					from gavo.protocols import sdm
				</code>
			</setup>
			<code>
				# copy over columns and params from the instance table as
				# params for us.
				ssapInstance = context.resolveId(mixinPars["ssaTable"])
				for col in ssapInstance.columns:
					atts = col.getAttributes()
					atts["utype"] = sdm.getSDM1UtypeForSSA(atts["utype"])
					atts["required"] = False
					substrate.feedObject("param",
						base.makeStruct(rscdef.Param, parent_=substrate, **atts))
				for param in ssapInstance.params:
					newUtype = sdm.getSDM1UtypeForSSA(param.utype)
					substrate.feedObject("param",
						param.change(utype=newUtype))

				specUnit = getattr(
					ssapInstance,
					"from_mixin_spectral_unit",
					"\spectralUnitOverride")
				if specUnit is None:
					raise base.StructureError("Cannot deduce spectral ucd/unit from"
						" ssa table; use spectralUCDOverride and spectralUnitOverride")

				specCol = substrate.getColumnByName("spectral")
				specCol.feed("ucd", r"\spectralUCDOverride" or
					ssapInstance.from_mixin_spectral_ucd)
				specCol.feed("unit", r"\spectralUnitOverride" or
					ssapInstance.from_mixin_spectral_unit)

				fluxUnit = getattr(ssapInstance,
					"from_mixin_flux_unit",
					"\fluxUnitOverride")
				if fluxUnit is None:
					raise base.StructureError("Cannot deduce flux ucd/unit from"
						" ssa table; use fluxUCDOverride and fluxUnitOverride")
				if specUnit is None:
					raise base.StructureError("Cannot deduce flux ucd/unit from"
						" ssa table; use fluxUCDOverride and fluxUnitOverride")

				fluxCol = substrate.getColumnByName("flux")
				fluxCol.feed("ucd", r"\fluxUCDOverride"
					or ssapInstance.from_mixin_flux_ucd)
				fluxCol.feed("unit", r"\fluxUnitOverride"
					or ssapInstance.from_mixin_flux_unit)

				# set the SDM container meta if not already present
				if substrate.getMeta("utype", default=None) is None:
					substrate.setMeta("utype", "spec:Spectrum")
			</code>
		</processEarly>
	</mixinDef>

	<STREAM id="_coverage-contents">
		<column name="ssa_region" type="spoly"
			ucd="pos.outline;obs.field"
			tablehead="Coverage"
			description="Rough coverage based on location and aperture."
			verbLevel="30"/>
		<index columns="ssa_region" method="GIST"/>
	</STREAM>

	<mixinDef id="simpleCoverage">
		<doc>
			A mixin furnishes a table with an ssa_region column giving
			a polygonal coverage.  For SSA itself, that's unnecessary, but it's
			highly recommended if you have data with positional and aperture
			data and will publish it via obscore, too (which in turn is highly
			recommended).

			The column will be filled with a hexagon approximating the aperture.
			This is done by //ssap#fill-plainlocation (or, historically, by
			//ssap#setMeta), so usually you're all set with this mixin.  We also
			create an index for the ssa_region field.

			To make it visible in obscore, you must bind the ``ssa_region`` parameter
			of `the //ssap#view mixin`_ to ``ssa_region`` (so the column is in the
			SSAP table, and the ``coverage`` mixin par of `the
			//obscore#publishSSAPMIXC mixin`_ to ``ssa_region`` (so the value ends up
			in obscore's ``s_region``).
		</doc>
		<events>
			<FEED source="_coverage-contents"/>
		</events>
	</mixinDef>

	<mixinDef id="plainlocation">
		<doc>
			A mixin that adds ssa_location column to a table.

			You probably want this in the source tables for //ssap#view tables.
			This will also index the column.  At least if you later want to
			publish the data through obscore, you will also want
			`the //ssap#simpleCoverage mixin`_ if you mix this in.

			Use the `//ssap#fill-plainlocation`_ apply to feed these.
		</doc>

		<events>
			<column name="ssa_location" type="spoint"
				ucd="pos.eq" unit="deg"
				utype="ssa:Char.SpatialAxis.Coverage.Location.Value"
				tablehead="Location"
				description="ICRS location of aperture center"
				verbLevel="5"/>
			<index columns="ssa_location" method="GIST"/>
		</events>
	</mixinDef>

	<procDef type="apply" id="fill-plainlocation">
		<doc>
			This mixin fills the columns added by the plainlocation mixin
			with values generated from ra, dec, and aperture.
		</doc>
		<setup>
			<par key="ra" late="True" description="ICRS RA of aperture center"/>
			<par key="dec" late="True" description="ICRS Dec of aperture center"/>
			<par key="aperture" late="True"
				description="Size of the aperture in degrees; if you leave this
					at None, no ssa_region will be generated.">None</par>
		</setup>
		<code>
			vars["ssa_location"] = vars["ssa_region"] = None
			if ra is None or dec is None:
				return

			vars["ssa_location"] = pgsphere.SPoint.fromDegrees(ra, dec)
			if aperture is not None:
				vars["ssa_region"] = pgsphere.SCircle.fromDALI(
						[ra, dec, aperture/2]).asPoly(6)
		</code>
	</procDef>

	<procDef type="apply" id="feedSSAToSDM">
		<doc>
			feedSSAToSDM takes the current rowIterator's sourceToken and
			feeds it to the params of the current target.  sourceTokens must
			be an SSA rowdict (as provided by the sdmCore).  Further, it takes
			the params from the sourceTable argument and feeds them to the
			params, too.

			All this probably only makes sense in parmakers when making tables
			mixing in //ssap#sdm-instance in data children of sdmCores.
		</doc>
		<code>
			srcMeta = vars["parser_"].sourceToken
			for param in targetTable.tableDef.params:
				newVal = srcMeta.get(param.name)
				if newVal is not None:
					targetTable.setParam(param.name, newVal)
		</code>
	</procDef>

	<STREAM id="obscore-time-index">
		<doc>
			Include this stream in an ssa-like table feeding obscore.  It will
			add an index useful for querying against obscore t_min and t_max.

			This uses the dateObs and timeExt attributes which are
			preset for what the columns are called in SSA tables.  dateObs
			must be a column reference because we declare the index to be on
			it.  timeExt can be an expression, too, and no index will be declared
			on it.
		</doc>
		<DEFAULTS
			dateObs="ssa_dateObs"
			timeExt="ssa_timeExt"/>
		<index name="t_min_index" columns="\dateObs"
			>(\dateObs-\timeExt/43200.)</index>
		<index name="t_max_index" columns="\dateObs"
			>(\dateObs+\timeExt/43200.)</index>
	</STREAM>

</resource>
