Commit 4d0d1330 authored by langurmonkey's avatar langurmonkey

First functional integration with SAMP

This commit contains the first functional integration of Gaia Sky with [SAMP](http://www.ivoa.net/documents/SAMP/).
Since Gaia Sky only displays 3D positional information, and knows how to parse
stars, there are a few restrictions as to how the integration with SAMP is implemneted.

The current implementation only allows using Gaia Sky as a SAMP client. This means that
when Gaia Sky is started, it looks for a preexisting SAMP hub. If it is found, then
a connection is attempted. If it is not found, then Gaia Sky will attempt further
connections in the future using a specified interval of 10 seconds. Gaia Sky will
never run its own SAMP hub, so you will always need a SAMP-hub application (Topcat,
Aladin, etc.) to use the interoperability that SAMP offers.

Also, the only supported format is VOTable. There are, however, some restrictions
as to how the VOTables received through SAMP are interpreted and used (if at all)
by Gaia Sky:

- For the **positional data**, Gaia Sky will look for spherical and cartesian coordinates. In the
case of spherical coordinates, equatorial (`pos.eq.ra`, `pos.eq.dec`), galactic (`pos.galactic.lon`, `pos.galactic.lat`) and
ecliptic (`pos.ecliptic.lon`, `pos.ecliptic.lat`) are supported. To work out the distance, it looks for `pos.parallax` and
`pos.distance`. If either of those are found, they are used. Otherwise, a default parallax of 0.04 mas is used. With
respect to cartesian coordinates, it recognizes `pos.cartesian.x|y|z`, and they are interpreted in the equatorial
system by default.
If no UCDs are available, only equatorial coordinates (ra, dec) are supported, and they are looked up using
the column names.
- **Proper motions** are not yet supported via SAMP.
- **Magnituded** are supported using the `phot.mag` or `phot.mag;stat.mean` UCDs. Otherwise, they are
discovered using the column names `mag`, `bmag`, `gmag`, `phot_g_mean_mag`. If no magnitudes are found,
the default value of 15 is used.
- **Colors** are discovered using the `phot.color` UCD. If not present, the column names `b_v`, `v_i`,
`bp_rp`, `bp_g` and `g_rp` are used, if present. If no color is discovered at all, the default value of 0.656 is used.
- Other physical quantities (mass, flux, T_eff, radius, etc.) are not yet supported via SAMP.

This commit fixes #246
parent 362532c3
......@@ -78,7 +78,7 @@ public class SDSSDataProvider implements IParticleGroupDataProvider {
double dist = ((z * 299792.46) / 71);
if (dist > 16) {
// Convert position
Position p = new Position(ra, "deg", dec, "deg", dist, "mpc", PositionType.RA_DEC_DIST);
Position p = new Position(ra, "deg", dec, "deg", dist, "mpc", PositionType.EQ_SPH_DIST);
p.gsposition.scl(Constants.PC_TO_U);
point[0] = p.gsposition.x;
point[1] = p.gsposition.y;
......
package gaia.cu9.ari.gaiaorbit.data.group;
import java.io.InputStream;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
......@@ -18,9 +17,8 @@ import gaia.cu9.ari.gaiaorbit.util.Logger;
import gaia.cu9.ari.gaiaorbit.util.color.ColourUtils;
import gaia.cu9.ari.gaiaorbit.util.coord.Coordinates;
import gaia.cu9.ari.gaiaorbit.util.math.Vector3d;
import gaia.cu9.ari.gaiaorbit.util.ucd.UCDParser;
import gaia.cu9.ari.gaiaorbit.util.units.Position;
import gaia.cu9.ari.gaiaorbit.util.units.Position.PositionType;
import uk.ac.starlink.table.ColumnInfo;
import uk.ac.starlink.table.StarTable;
import uk.ac.starlink.table.StarTableFactory;
import uk.ac.starlink.table.TableSequence;
......@@ -61,13 +59,7 @@ public class STILDataProvider extends AbstractStarGroupDataProvider {
public Array<? extends ParticleBean> loadData(DataSource ds, double factor) {
try {
Map<String, ColumnInfo> ucds = new HashMap<String, ColumnInfo>();
Map<String, Integer> ucdsi = new HashMap<String, Integer>();
Map<String, ColumnInfo> colnames = new HashMap<String, ColumnInfo>();
Map<String, Integer> colnamesi = new HashMap<String, Integer>();
TableSequence ts = factory.makeStarTables(ds);
// Find table
List<StarTable> tables = new LinkedList<StarTable>();
......@@ -80,213 +72,91 @@ public class STILDataProvider extends AbstractStarGroupDataProvider {
table = t;
}
}
Logger.info(this.getClass().getSimpleName(), "Selected table " + table.getName() + ": " + table.getRowCount() + " elements");
initLists((int) table.getRowCount());
int count = table.getColumnCount();
ColumnInfo[] colInfo = new ColumnInfo[count];
for (int i = 0; i < count; i++) {
colInfo[i] = table.getColumnInfo(i);
ucds.put(colInfo[i].getUCD(), colInfo[i]);
ucdsi.put(colInfo[i].getUCD(), i);
colnames.put(colInfo[i].getName(), colInfo[i]);
colnamesi.put(colInfo[i].getName(), i);
}
/** POSITION **/
ColumnInfo ac = null, bc = null, cc = null;
int ai, bi, ci;
PositionType type = null;
int tychoi = -1;
int hipi = -1;
// Check positions
if (ucds.containsKey("pos.eq.ra")) {
// RA_DEC_DIST or RA_DEC_PLX
ac = ucds.get("pos.eq.ra");
bc = ucds.get("pos.eq.dec");
cc = ucds.containsKey("pos.parallax") ? ucds.get("pos.parallax") : ucds.get("pos.distance");
type = ucds.containsKey("pos.parallax") ? PositionType.RA_DEC_PLX : PositionType.RA_DEC_DIST;
}
if (type == null && ucds.containsKey("pos.galactic.lon")) {
// GLON_GLAT_DIST or GLON_GLAT_PLX
ac = ucds.get("pos.galactic.lon");
bc = ucds.get("pos.galactic.lat");
cc = ucds.containsKey("pos.parallax") ? ucds.get("pos.parallax") : ucds.get("pos.distance");
type = ucds.containsKey("pos.parallax") ? PositionType.GLON_GLAT_PLX : PositionType.GLON_GLAT_DIST;
}
if (type == null && ucds.containsKey("pos.eq.x")) {
// Equatorial XYZ
ac = ucds.get("pos.eq.x");
bc = ucds.get("pos.eq.y");
cc = ucds.get("pos.eq.z");
type = PositionType.XYZ_EQUATORIAL;
}
if (type == null && ucds.containsKey("pos.galactic.x")) {
// Galactic XYZ
ac = ucds.get("pos.galactic.x");
bc = ucds.get("pos.galactic.y");
cc = ucds.get("pos.galactic.z");
type = PositionType.XYZ_GALACTIC;
}
if (type == null) {
throw new RuntimeException("Could not find suitable position candidate columns");
} else {
ai = ucdsi.get(ac.getUCD());
bi = ucdsi.get(bc.getUCD());
ci = ucdsi.get(cc.getUCD());
}
/** CHECK FOR TYCHO NUMBER **/
if (colnames.containsKey("TYC")) {
tychoi = colnamesi.get("TYC");
}
/** CHECK FOR HIP NUMBER **/
if (colnames.containsKey("HIP")) {
hipi = colnamesi.get("HIP");
}
/** APP MAGNITUDE **/
ColumnInfo magc = null;
int magi;
if (ucds.containsKey("phot.mag;em.opt.V")) {
magc = ucds.get("phot.mag;em.opt.V");
} else if (ucds.containsKey("phot.mag;em.opt.B")) {
magc = ucds.get("phot.mag;em.opt.B");
} else if (ucds.containsKey("phot.mag;em.opt.I")) {
magc = ucds.get("phot.mag;em.opt.I");
} else if (ucds.containsKey("phot.mag;em.opt.R")) {
magc = ucds.get("phot.mag;em.opt.R");
} else if (ucds.containsKey("phot.mag;stat.mean;em.opt")) {
magc = ucds.get("phot.mag;stat.mean;em.opt");
} else {
throw new RuntimeException("Could not find suitable magnitude candidate column");
}
magi = ucdsi.get(magc.getUCD());
/** ABS MAGNITUDE **/
ColumnInfo abmagc = null;
int abmagi = -1;
if (ucds.containsKey("phys.magAbs;em.opt.V")) {
abmagc = ucds.get("phys.magAbs;em.opt.V");
} else if (ucds.containsKey("phys.magAbs;em.opt.B")) {
abmagc = ucds.get("phys.magAbs;em.opt.B");
} else if (ucds.containsKey("phys.magAbs;em.opt.I")) {
abmagc = ucds.get("phys.magAbs;em.opt.I");
} else if (ucds.containsKey("phys.magAbs;em.opt.R")) {
abmagc = ucds.get("phys.magAbs;em.opt.R");
}
if (abmagc != null)
abmagi = ucdsi.get(abmagc.getUCD());
/** COLOR **/
ColumnInfo colc = null;
int coli = -1;
if (ucds.containsKey("phot.color;em.opt.B;em.opt.V")) {
// B-V
colc = ucds.get("phot.color;em.opt.B;em.opt.V");
}
if (colc != null) {
coli = ucdsi.get(colc.getUCD());
}
/** NAME **/
ColumnInfo idstrc = null;
ColumnInfo idc = null;
int idstri = 0, idi = 0;
if (ucds.containsKey("meta.id")) {
idc = ucds.get("meta.id");
idi = ucdsi.get("meta.id");
}
if (ucds.containsKey("meta.id;meta.main")) {
idc = ucds.get("meta.id;meta.main");
idi = ucdsi.get("meta.id;meta.main");
}
long rowcount = table.getRowCount();
for (long i = 0; i < rowcount; i++) {
Object[] row = table.getRow(i);
String tycho = "";
if (tychoi >= 0 && row[tychoi] != null)
tycho = (String) row[tychoi];
int hip = -1;
if (hipi >= 0 && row[hipi] != null)
hip = ((Number) row[hipi]).intValue();
/** POSITION **/
double a = ((Number) row[ai]).doubleValue();
double b = ((Number) row[bi]).doubleValue();
double c = ((Number) row[ci]).doubleValue();
Position p = new Position(a, ac.getUnitString(), b, bc.getUnitString(), c, cc == null ? "" : cc.getUnitString(), type);
double distpc = p.gsposition.len();
p.gsposition.scl(Constants.PC_TO_U);
// Find out RA/DEC/Dist
Vector3d sph = new Vector3d();
Coordinates.cartesianToSpherical(p.gsposition, sph);
double appmag = ((Number) row[magi]).floatValue();
double absmag;
if (abmagi >= 0) {
absmag = ((Number) row[abmagi]).floatValue();
} else {
absmag = (appmag - 2.5 * Math.log10(Math.pow(distpc / 10d, 2d)));
}
double flux = Math.pow(10, -absmag / 2.5f);
double size = Math.min((Math.pow(flux, 0.5f) * Constants.PC_TO_U * 0.16f), 1e9f) / 1.5;
float bv = coli > 0 ? ((Number) row[coli]).floatValue() : 0.656f;
float[] rgb = ColourUtils.BVtoRGB(bv);
double col = Color.toFloatBits(rgb[0], rgb[1], rgb[2], 1.0f);
Long id = (idc == null || !idc.getContentClass().equals(Long.class)) ? ++starid : ((Number) row[idi]).longValue();
String idstr = null;
if (idstrc == null || !idstrc.getContentClass().isAssignableFrom(String.class)) {
// ID string from catalog if possible
if (hipi >= 0 && row[hipi] != null) {
idstr = "HIP" + row[hipi];
} else if (tychoi >= 0 && row[tychoi] != null) {
idstr = "TYC" + row[tychoi];
UCDParser ucdp = new UCDParser();
ucdp.parse(table);
if (ucdp.haspos) {
long rowcount = table.getRowCount();
for (long i = 0; i < rowcount; i++) {
Object[] row = table.getRow(i);
/** POSITION **/
double a = ((Number) row[ucdp.POS1.index]).doubleValue();
double b = ((Number) row[ucdp.POS2.index]).doubleValue();
// Default parallax is 0.04
double c = ucdp.POS3 != null ? ((Number) row[ucdp.POS3.index]).doubleValue() : 0.04;
String unitc = ucdp.POS3 != null ? ucdp.POS3.unit : "mas";
Position p = new Position(a, ucdp.POS1.unit, b, ucdp.POS2.unit, c, unitc, ucdp.postype);
double distpc = p.gsposition.len();
p.gsposition.scl(Constants.PC_TO_U);
// Find out RA/DEC/Dist
Vector3d sph = new Vector3d();
Coordinates.cartesianToSpherical(p.gsposition, sph);
/** MAGNITUDE **/
double appmag;
if(ucdp.hasmag) {
appmag = ((Number) row[ucdp.MAG.index]).floatValue();
}else {
// Default magnitude
appmag = 15;
}
double absmag = (appmag - 2.5 * Math.log10(Math.pow(distpc / 10d, 2d)));
double flux = Math.pow(10, -absmag / 2.5f);
double size = Math.min((Math.pow(flux, 0.5f) * Constants.PC_TO_U * 0.16f), 1e9f) / 1.5;
/** COLOR **/
float color;
if (ucdp.hascol) {
color = ((Number) row[ucdp.COL.index]).floatValue();
} else {
idstr = id.toString();
// Default color
color = 0.656f;
}
float[] rgb = ColourUtils.BVtoRGB(color);
double col = Color.toFloatBits(rgb[0], rgb[1], rgb[2], 1.0f);
/** IDENTIFIER AND NAME **/
String name;
Long id;
if(ucdp.hasid) {
name = row[ucdp.ID.index].toString();
id = ++starid;
}else {
id = ++starid;
name = id.toString();
}
} else {
idstr = (String) row[idstri];
}
double[] point = new double[StarBean.SIZE];
point[StarBean.I_HIP] = -1;
point[StarBean.I_TYC1] = -1;
point[StarBean.I_TYC2] = -1;
point[StarBean.I_TYC3] = -1;
point[StarBean.I_X] = p.gsposition.x;
point[StarBean.I_Y] = p.gsposition.y;
point[StarBean.I_Z] = p.gsposition.z;
point[StarBean.I_PMX] = 0;
point[StarBean.I_PMY] = 0;
point[StarBean.I_PMZ] = 0;
point[StarBean.I_MUALPHA] = 0;
point[StarBean.I_MUDELTA] = 0;
point[StarBean.I_RADVEL] = 0;
point[StarBean.I_COL] = col;
point[StarBean.I_SIZE] = size;
//point[StarBean.I_RADIUS] = radius;
//point[StarBean.I_TEFF] = teff;
point[StarBean.I_APPMAG] = appmag;
point[StarBean.I_ABSMAG] = absmag;
list.add(new StarBean(point, id, idstr));
double[] point = new double[StarBean.SIZE];
point[StarBean.I_HIP] = -1;
point[StarBean.I_TYC1] = -1;
point[StarBean.I_TYC2] = -1;
point[StarBean.I_TYC3] = -1;
point[StarBean.I_X] = p.gsposition.x;
point[StarBean.I_Y] = p.gsposition.y;
point[StarBean.I_Z] = p.gsposition.z;
point[StarBean.I_PMX] = 0;
point[StarBean.I_PMY] = 0;
point[StarBean.I_PMZ] = 0;
point[StarBean.I_MUALPHA] = 0;
point[StarBean.I_MUDELTA] = 0;
point[StarBean.I_RADVEL] = 0;
point[StarBean.I_COL] = col;
point[StarBean.I_SIZE] = size;
//point[StarBean.I_RADIUS] = radius;
//point[StarBean.I_TEFF] = teff;
point[StarBean.I_APPMAG] = appmag;
point[StarBean.I_ABSMAG] = absmag;
list.add(new StarBean(point, id, name));
}
} else {
Logger.error("Table not loaded: Position not found");
}
} catch (Exception e) {
......
......@@ -84,7 +84,7 @@ public class STILCatalogLoader extends AbstractCatalogLoader {
ac = ucds.get("pos.eq.ra");
bc = ucds.get("pos.eq.dec");
cc = ucds.containsKey("pos.parallax.trig") ? ucds.get("pos.parallax.trig") : ucds.get("pos.distance");
type = ucds.containsKey("pos.parallax.trig") ? PositionType.RA_DEC_PLX : PositionType.RA_DEC_DIST;
type = ucds.containsKey("pos.parallax.trig") ? PositionType.EQ_SPH_PLX : PositionType.EQ_SPH_DIST;
}
if (type == null && ucds.containsKey("pos.galactic.lon")) {
......@@ -92,7 +92,7 @@ public class STILCatalogLoader extends AbstractCatalogLoader {
ac = ucds.get("pos.galactic.lon");
bc = ucds.get("pos.galactic.lat");
cc = ucds.containsKey("pos.parallax.trig") ? ucds.get("pos.parallax.trig") : ucds.get("pos.distance");
type = ucds.containsKey("pos.parallax.trig") ? PositionType.GLON_GLAT_PLX : PositionType.GLON_GLAT_DIST;
type = ucds.containsKey("pos.parallax.trig") ? PositionType.GAL_SPH_PLX : PositionType.GAL_SPH_DIST;
}
if (type == null && ucds.containsKey("pos.eq.x")) {
......@@ -100,7 +100,7 @@ public class STILCatalogLoader extends AbstractCatalogLoader {
ac = ucds.get("pos.eq.x");
bc = ucds.get("pos.eq.y");
cc = ucds.get("pos.eq.z");
type = PositionType.XYZ_EQUATORIAL;
type = PositionType.EQ_XYZ;
}
if (type == null && ucds.containsKey("pos.galactic.x")) {
......@@ -108,7 +108,7 @@ public class STILCatalogLoader extends AbstractCatalogLoader {
ac = ucds.get("pos.galactic.x");
bc = ucds.get("pos.galactic.y");
cc = ucds.get("pos.galactic.z");
type = PositionType.XYZ_GALACTIC;
type = PositionType.GAL_XYZ;
}
if (type == null) {
......
......@@ -79,7 +79,7 @@ public class Galaxy extends Particle {
// Calculate size - This contains arbitrary boundary values to make
// things nice on the render side
size = (float) Math.min(.3e11, (Math.max((Math.pow(flux, 0.5f) * Constants.PC_TO_U * 1e-3), .6e9f) * 4.5e0d));
size = (float) (Math.max((Math.pow(flux, 0.5f) * Constants.PC_TO_U * 1e-3), .6e9f) * 4.5e0d) * 2.5f;
computedSize = 0;
}
......
......@@ -562,6 +562,10 @@ public class ParticleGroup extends FadeNode implements I3DTextRenderable, IFocus
}
public int getCandidateIndex() {
return candidateFocusIndex;
}
@Override
public long getCandidateId() {
return getId();
......
package gaia.cu9.ari.gaiaorbit.util.samp;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
......@@ -13,6 +12,7 @@ import org.astrogrid.samp.client.ClientProfile;
import org.astrogrid.samp.client.DefaultClientProfile;
import org.astrogrid.samp.client.HubConnection;
import org.astrogrid.samp.client.HubConnector;
import org.astrogrid.samp.client.SampException;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.utils.Array;
......@@ -32,9 +32,11 @@ import uk.ac.starlink.util.URLDataSource;
public class SAMPClient implements IObserver {
private static final String ENV_VAR = "SAMP_HUB";
private static final String VAR_PREFIX = "std-lockurl:";
private static final String LOCKFILE = ".samp";
/**
* Whether to prefetch tables when a row selection is issued, even if
* table was not sent initially.
*/
private static final boolean PREFETCH = false;
private static SAMPClient instance;
......@@ -46,11 +48,13 @@ public class SAMPClient implements IObserver {
private HubConnector conn;
private STILDataProvider provider;
private Map<String, StarGroup> sgMap;
private Map<String, StarGroup> mapIdSg;
private Map<String, String> mapIdUrl;
private boolean preventProgrammaticEvents = false;
public SAMPClient() {
super();
EventManager.instance.subscribe(this, Events.DISPOSE);
EventManager.instance.subscribe(this, Events.FOCUS_CHANGED, Events.DISPOSE);
}
public void initialize() {
......@@ -59,12 +63,13 @@ public class SAMPClient implements IObserver {
// Init provider
provider = new STILDataProvider();
// Init map
sgMap = new HashMap<String, StarGroup>();
mapIdSg = new HashMap<String, StarGroup>();
mapIdUrl = new HashMap<String, String>();
ClientProfile cp = DefaultClientProfile.getProfile();
HubConnector conn = new GaiaSkyHubConnector(cp);
conn = new GaiaSkyHubConnector(cp);
// Configure it with metadata about this application
Metadata meta = new Metadata();
......@@ -87,33 +92,13 @@ public class SAMPClient implements IObserver {
String name = (String) msg.getParam("name");
String id = (String) msg.getParam("table-id");
String url = (String) msg.getParam("url");
Logger.info("Load VOTable: " + msg.getParam("id"));
try {
DataSource ds = new URLDataSource(new URL(url));
@SuppressWarnings("unchecked")
Array<StarBean> data = (Array<StarBean>) provider.loadData(ds, 1.0f);
StarGroup sg = new StarGroup();
sg.setName(id);
sg.setParent("Universe");
sg.setFadeout(new double[] { 21e2, .5e8 });
sg.setLabelcolor(new double[] { 1.0, 1.0, 1.0, 1.0 });
sg.setColor(new double[] { 1.0, 1.0, 1.0, 0.25 });
sg.setSize(6.0);
sg.setLabelposition(new double[] { 0.0, -5.0e7, -4e8 });
sg.setCt("Stars");
sg.setData(data);
sg.doneLoading(null);
sgMap.put(id, sg);
// Insert
Gdx.app.postRunnable(() -> {
GaiaSky.instance.sg.insert(sg, true);
});
} catch (MalformedURLException e) {
Logger.error(e);
boolean loaded = loadVOTable(url, id, name);
if (loaded) {
Logger.info(SAMPClient.class.getSimpleName(), "VOTable " + name + " loaded successfully");
} else {
Logger.info(SAMPClient.class.getSimpleName(), "Error loading VOTable " + name);
}
return null;
......@@ -127,12 +112,24 @@ public class SAMPClient implements IObserver {
Long row = Parser.parseLong((String) msg.getParam("row"));
String id = (String) msg.getParam("table-id");
String url = (String) msg.getParam("url");
Logger.info("Select row " + row + " of " + id);
if (sgMap.containsKey(id)) {
StarGroup sg = sgMap.get(id);
sg.setFocusIndex(row.intValue());
EventManager.instance.post(Events.FOCUS_CHANGE_CMD, sg);
// First, fetch table if not here
boolean loaded = mapIdSg.containsKey(id);
if (!loaded && PREFETCH) {
loaded = loadVOTable(url, id, id);
}
// If table here, select
if (loaded) {
Logger.info(SAMPClient.class.getSimpleName(), "Select row " + row + " of " + id);
if (mapIdSg.containsKey(id)) {
StarGroup sg = mapIdSg.get(id);
sg.setFocusIndex(row.intValue());
preventProgrammaticEvents = true;
EventManager.instance.post(Events.FOCUS_CHANGE_CMD, sg);
preventProgrammaticEvents = false;
}
}
return null;
}
......@@ -142,7 +139,7 @@ public class SAMPClient implements IObserver {
conn.addMessageHandler(new AbstractMessageHandler("table.select.rowList") {
public Map processCall(HubConnection c, String senderId, Message msg) {
// do stuff
Logger.info("Select rows");
Logger.info(SAMPClient.class.getSimpleName(), "Select rows");
return null;
}
});
......@@ -151,7 +148,7 @@ public class SAMPClient implements IObserver {
conn.addMessageHandler(new AbstractMessageHandler("coord.pointAt.sky") {
public Map processCall(HubConnection c, String senderId, Message msg) {
// do stuff
Logger.info("Point to coordinate");
Logger.info(SAMPClient.class.getSimpleName(), "Point to coordinate");
return null;
}
});
......@@ -167,9 +164,71 @@ public class SAMPClient implements IObserver {
}
/**
* Loads a VOTable into a star group
* @param url The URL to fetch the table
* @param id The table id
* @param name The table name
* @return Boolean indicating whether loading succeeded or not
*/
private boolean loadVOTable(String url, String id, String name) {
Logger.info(SAMPClient.class.getSimpleName(), "Loading VOTable: " + name + " from " + url);
try {
DataSource ds = new URLDataSource(new URL(url));
@SuppressWarnings("unchecked")
Array<StarBean> data = (Array<StarBean>) provider.loadData(ds, 1.0f);
StarGroup sg = new StarGroup();
sg.setName(id);
sg.setParent("Universe");