All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.sf.fmj.filtergraph.SimpleGraphBuilder Maven / Gradle / Ivy

There is a newer version: 1.0.2-jitsi
Show newest version
package net.sf.fmj.filtergraph;

import java.util.*;

import javax.media.*;
import javax.media.format.*;

import net.sf.fmj.media.*;

/**
 *
 * This is the Graph builder to generate the data flow graph for rendering an
 * input format.
 *
 * It contains 3 parts: 1) Routines to search for all the supported output
 * formats; 2) Routines to build a default flow graph -- buildGraph;
 *
 * A default graph is such that no customised option is specified on the
 * TrackControl.
 *
 * It operates on a breath-first search algorithm until the final target is
 * reached as defined by the findTarget() method. Intermediate search
 * paths are stored as GraphNodes in the "candidates" vector.
 */
public class SimpleGraphBuilder
{
    static public PlugIn createPlugIn(String name, int type)
    {
        Class cls;
        Object obj;

        try
        {
            // cls = Class.forName(name);
            cls = BasicPlugIn.getClassForName(name);
            obj = cls.newInstance();
        } catch (Exception e)
        {
            // Log.write("Cannot instantiate: " + name);
            return null;
        } catch (Error e)
        {
            return null;
        }

        if (verifyClass(obj, type))
            return (PlugIn) obj;
        // Log.write(name + " is not of type " + cls);
        return null;
    }

    /**
     * Find a codec that can handle the given input and output. The output
     * argument can be null if no specific output format is required.
     */
    static public Codec findCodec(Format in, Format out, Format selectedIn[],
            Format selectedOut[])
    {
        Vector cnames = PlugInManager.getPlugInList(in, out,
                PlugInManager.CODEC);
        if (cnames == null)
        {
            // Well no codec supports that input. :(
            return null;
        }

        Codec c = null;
        Format fmts[], matched;
        for (int i = 0; i < cnames.size(); i++)
        {
            if ((c = (Codec) createPlugIn((String) cnames.elementAt(i),
                    PlugInManager.CODEC)) == null)
                continue;
            fmts = c.getSupportedInputFormats();
            if ((matched = matches(in, fmts, null, c)) == null)
                continue;
            if (selectedIn != null && selectedIn.length > 0)
                selectedIn[0] = matched;
            fmts = c.getSupportedOutputFormats(matched);
            if (fmts == null || fmts.length == 0)
            {
                // Weird!
                continue;
            }
            boolean success = false;
            for (int j = 0; j < fmts.length; j++)
            {
                // Try out the supported output formats in turn.
                if (out != null)
                {
                    if (!out.matches(fmts[j])
                            || (matched = out.intersects(fmts[j])) == null)
                        continue;
                } else
                    matched = fmts[j];
                if (c.setOutputFormat(matched) != null)
                {
                    success = true;
                    break;
                }
            }
            if (success)
            {
                try
                {
                    c.open();
                } catch (ResourceUnavailableException e)
                {
                }
                if (selectedOut != null && selectedOut.length > 0)
                    selectedOut[0] = matched;
                // Alright, we are done!
                return c;
            }
        }

        return null;
    }

    /**
     * Find a renderer that can handle the given input and output. The output
     * argument can be null if no specific output format is required.
     */
    static public Renderer findRenderer(Format in)
    {
        Vector names = PlugInManager.getPlugInList(in, null,
                PlugInManager.RENDERER);
        if (names == null)
        {
            // Well no renderer supports that input. :(
            return null;
        }

        Renderer r = null;
        Format fmts[], matched;
        for (int i = 0; i < names.size(); i++)
        {
            if ((r = (Renderer) createPlugIn((String) names.elementAt(i),
                    PlugInManager.RENDERER)) == null)
                continue;
            fmts = r.getSupportedInputFormats();
            if ((matched = matches(in, fmts, null, r)) == null)
                continue;

            try
            {
                r.open();
            } catch (ResourceUnavailableException e)
            {
            }

            // Alright, we are done!
            return r;
        }

        return null;
    }

    /**
     * Return a chain of codecs and renderer to render to input format. Unlike
     * findCodec and findRenderer, it uses the same graph building algorithm
     * that the media engine uses to determine the best rendering path for a
     * particular input format. The return value is a vector of plugins of all
     * the codecs and the renderer. The plugin list is in reverse order starting
     * from the renderer. The list of the corresponding input formats for each
     * codec is also returned as an argument to the function.
     */
    static public Vector findRenderingChain(Format in, Vector formats)
    {
        SimpleGraphBuilder gb = new SimpleGraphBuilder();
        GraphNode n;

        if ((n = gb.buildGraph(in)) == null)
            return null;

        Vector list = new Vector(10);

        while (n != null && n.plugin != null)
        {
            list.addElement(n.plugin);
            if (formats != null)
                formats.addElement(n.input);
            n = n.prev;
        }

        return list;
    }

    /**
     * Given a codec class name, instantiate the codec and query it dynamically
     * to see if it supports the given input and output formats.
     */
    static public GraphNode getPlugInNode(String name, int type,
            Map plugIns)
    {
        GraphNode gn = null;

        // Check the hash registry to see if we've already instantiated that
        // object. If not, we'll instantiate it.
        if (plugIns == null || (gn = plugIns.get(name)) == null)
        {
            PlugIn p = createPlugIn(name, type);

            gn = new GraphNode(name, p, null, null, 0);
            if (plugIns != null)
                plugIns.put(name, gn);

            if (p == null)
            {
                // If we failed to create it this time, we won't try it again.
                // We'll mark it as failed.
                gn.failed = true;
                return null;
            } else
                return gn;
        }

        // If it has been marked as failed before, we won't attempt
        // to use it again.
        if (gn.failed)
            return null;

        if (verifyClass(gn.plugin, type))
            return gn;

        return null;
    }

    /**
     * Choose a format among the two input arrays that matches and verify that
     * if the given upstream and downstream plugins accept the matched format as
     * output (for the upstream) or as input (for the downstream). Either of the
     * plugin arguments can be null. In which case the verification step will be
     * skipped accordingly.
     *
     * @param outs the supported output formats from the upstream node.
     * @param ins the supported input formats from the downstream node.
     * @param up the upstream node.
     * @param down the downstream node.
     * @return a matching format.
     */
    static public Format matches(Format outs[], Format ins[], PlugIn up,
            PlugIn down)
    {
        Format fmt;
        if (outs == null)
            return null;
        for (int i = 0; i < outs.length; i++)
        {
            if ((fmt = matches(outs[i], ins, up, down)) != null)
                return fmt;
        }
        return null;
    }

    /**
     * Choose a format among the two input arrays that matches and verify that
     * if the given upstream and downstream plugins accept the matched format as
     * output (for the upstream) or as input (for the downstream). Either of the
     * plugin arguments can be null. In which case the verification step will be
     * skipped accordingly.
     *
     * @return a matching format.
     */
    static public Format matches(Format out, Format ins[], PlugIn up,
            PlugIn down)
    {
        if (out == null || ins == null)
            return null;
        for (int i = 0; i < ins.length; i++)
        {
            if (ins[i] != null
                    && ins[i].getClass().isAssignableFrom(out.getClass())
                    && out.matches(ins[i]))
            {
                Format fmt = out.intersects(ins[i]);

                if (fmt == null)
                    // weird!
                    continue;

                // Check if the downstream accepts the given input.
                if (down != null && (fmt = verifyInput(down, fmt)) == null)
                    continue;

                // Check if the upstream accepts the given as output.
                Format refined = fmt;
                if (up != null && (refined = verifyOutput(up, fmt)) == null)
                    continue;

                // If the returned output format from the upstream is
                // different from the original input to the upstream,
                // we'll have to check that new format on the downstream
                // to make sure.
                if (down != null && refined != fmt
                        && verifyInput(down, refined) == null)
                    continue;

                return refined;
            }
        }
        return null;
    }

    static public Format matches(Format outs[], Format in, PlugIn up,
            PlugIn down)
    {
        Format ins[] = new Format[1];
        ins[0] = in;
        return matches(outs, ins, up, down);
    }

    static public boolean verifyClass(Object obj, int type)
    {
        Class cls;

        switch (type)
        {
        case PlugInManager.CODEC:
            cls = Codec.class;
            break;
        case PlugInManager.RENDERER:
            cls = Renderer.class;
            break;
        case PlugInManager.MULTIPLEXER:
            cls = Multiplexer.class;
            break;
        default:
            cls = PlugIn.class;
        }

        if (cls.isInstance(obj))
            return true;
        else
            return false;
    }

    /**
     * Check if the given plugin supports the given input.
     */
    static public Format verifyInput(PlugIn p, Format in)
    {
        if (p instanceof Codec)
            return ((Codec) p).setInputFormat(in);
        if (p instanceof Renderer)
            return ((Renderer) p).setInputFormat(in);
        return null;
    }

    /**
     * Check if the given plugin supports the given output.
     */
    static public Format verifyOutput(PlugIn p, Format out)
    {
        if (p instanceof Codec)
            return ((Codec) p).setOutputFormat(out);
        return null;
    }

    // # of codec/converters allowed to use to complete a track.
    // damencho from 4 to 5
    protected int STAGES = 5;

    protected Hashtable plugIns
        = new Hashtable(40);

    protected GraphNode targetPlugins[] = null;

    protected Vector targetPluginNames = null;

    protected int targetType = -1;

    protected int indent = 0;

    // A non-published interface to trace the graph building process.
    static public GraphInspector inspector;

    static public void setGraphInspector(GraphInspector insp)
    {
        inspector = insp;
    }

    /**
     * Take a TrackControl and build the graph for it.
     */
    public boolean buildGraph(BasicTrackControl tc)
    {
        Log.comment("Input: " + tc.getOriginalFormat());

        Vector candidates = new Vector();
        GraphNode node = new GraphNode(null, (PlugIn) null,
                tc.getOriginalFormat(), null, 0);
        indent = 1;
        Log.setIndent(indent);

        // Define the final targets.
        if (!setDefaultTargets(tc.getOriginalFormat()))
            return false;

        candidates.addElement(node);

        GraphNode failed;

        while ((node = buildGraph(candidates)) != null)
        {
            // Found a potential graph. Check if we can build a
            // track from it.
            if ((failed = buildTrackFromGraph(tc, node)) == null)
            {
                // we are done.
                indent = 0;
                Log.setIndent(indent);
                return true;
            }

            // If we can't build a track from it, it's because there's
            // a node in the graph that cannot be opened. We'll have
            // to reap it from the candidates and the registry.
            removeFailure(candidates, failed, tc.getOriginalFormat());
        }

        indent = 0;
        Log.setIndent(indent);
        return false;
    }

    /**
     * Build a flow graph based on the given input format.
     */
    GraphNode buildGraph(Format input)
    {
        Log.comment("Input: " + input);

        Vector candidates = new Vector();
        GraphNode node = new GraphNode(null, (PlugIn) null, input, null, 0);
        indent = 1;
        Log.setIndent(indent);

        // Define the final targets.
        if (!setDefaultTargets(input))
            return null;

        candidates.addElement(node);

        GraphNode failed;

        while ((node = buildGraph(candidates)) != null)
        {
            // Found a potential graph. Verify it if all the
            // nodes can be used.
            if ((failed = verifyGraph(node)) == null)
            {
                // we are done.
                indent = 0;
                Log.setIndent(indent);
                return node;
            }

            // If we can't build a track from it, it's because there's
            // a node in the graph that cannot be opened. We'll have
            // to reap it from the candidates and the registry.
            removeFailure(candidates, failed, input);
        }

        indent = 0;
        Log.setIndent(indent);

        return node;
    }

    /**
     * Given the intermediate search candidates, build a graph until it reaches
     * a target.
     */
    protected GraphNode buildGraph(Vector candidates)
    {
        GraphNode node;
        while ((node = doBuildGraph(candidates)) == null)
        {
            if (candidates.isEmpty())
                break;
        }
        return node;
    }

    /**
     * When the graph build finds a viable graph to build, this callback will be
     * invoked to see if the graph can actually be built. Subclass should
     * implement this.
     */
    protected GraphNode buildTrackFromGraph(BasicTrackControl tc, GraphNode node)
    {
        return null;
    }

    /**
     * This is the "worker" method that does all the dirty work.
     */
    GraphNode doBuildGraph(Vector candidates)
    {
        if (candidates.isEmpty())
            return null;

        GraphNode node = (GraphNode) candidates.firstElement();
        candidates.removeElementAt(0);

        if (node.input == null
                && (node.plugin == null || !(node.plugin instanceof Codec)))
        {
            // shouldn't happen!
            Log.error("Internal error: doBuildGraph");
            return null;
        }

        int oldIndent = indent;

        Log.setIndent(node.level + 1);

//        Log.write("level: " + node.level);
        if (node.plugin != null)
        {
            // It may not seem necessary to do this since the previous round has
            // already verified the input. But since the same plugin could have
            // a different input called on it on previous rounds, it needs to be
            // reset to the designated input. This has caused a bug in failing
            // setOutputFormat for some codecs.
            if (verifyInput(node.plugin, node.input) == null)
                return null;
//            Log.write("Try plugin: " + node.plugin.getClass());
        } else
        {
//            Log.write("Given input: " + node.input);
        }

        // Stop when the target is reached as defined by the findTarget
        // method.
        GraphNode n;
        if ((n = findTarget(node)) != null)
        {
            // We are done!
//            Log.write(
//                    "Found target: "
//                        + ((n.plugin != null) ? n.plugin : n.cname));
            indent = oldIndent;
            Log.setIndent(indent);
            return n;
        }

        // Don't go deeper than allowed.
        if (node.level >= STAGES)
        {
            indent = oldIndent;
            Log.setIndent(indent);
            return null;
        }

        Format input, outs[];
        boolean mp3Pkt = false; // 2.1.1b hack -ivg

        if (node.plugin != null)
        {
            if (node.output != null)
            {
                outs = new Format[1];
                outs[0] = node.output;
            } else
            {
                outs = node.getSupportedOutputs(node.input);
                if (outs == null || outs.length == 0)
                {
//                    Log.write("Weird!  The given plugin does not support any output.");
                    indent = oldIndent;
                    Log.setIndent(indent);
                    return null;
                }
            }
            input = node.input;

            // 2.1.1b hack -ivg
//            if (node.plugin instanceof com.sun.media.codec.audio.mpa.Packetizer)
//                mp3Pkt = true;

        } else
        {
            outs = new Format[1];
            outs[0] = node.input;
            input = null;
        }

        GraphNode gn;
        Format fmt, ins[];
        boolean foundSomething = false;
        for (int i = 0; i < outs.length; i++)
        {
            // Ignore outputs that are the same as the input.
            if (!node.custom && input != null && input.equals(outs[i]))
                continue;

            // Verify the output format.
            if (node.plugin != null)
            {
                if (verifyOutput(node.plugin, outs[i]) == null)
                {
//                    Log.write("Verify output failed: " + node.plugin);
//                    Log.write("  with: " + outs[i]);
                    if (inspector != null && inspector.detailMode())
                        inspector.verifyOutputFailed(node.plugin, outs[i]);
                    continue;
                }

                if (inspector != null
                        && !inspector.verify((Codec) node.plugin, node.input,
                                outs[i]))
                    continue;
            }

//            Log.write("find codec for input: " + outs[i]);

            Vector cnames = PlugInManager.getPlugInList(outs[i], null,
                    PlugInManager.CODEC);
            if (cnames == null || cnames.size() == 0)
                continue;

            for (int j = 0; j < cnames.size(); j++)
            {
                // Instantiate and verify the codec.
                if ((gn = getPlugInNode((String) cnames.elementAt(j),
                        PlugInManager.CODEC, plugIns)) == null)
                    continue;

                // 2.1.1b hack -ivg
                // if (mp3Pkt && gn.plugin instanceof
                // com.sun.media.codec.audio.mpa.DePacketizer)
                // continue;

                // Check to see if the particular input/plugin combination
                // has already been attempted. If so, we don't need to
                // do it again.
                if (gn.checkAttempted(outs[i]))
                    continue;

//                Log.write("Try codec: " + cnames.elementAt(j));

                ins = gn.getSupportedInputs();
                if ((fmt = matches(outs[i], ins, null, gn.plugin)) == null)
                {
//                    Log.write("Verify input failed: " + outs[i]);
//                    Log.write("    : " + gn.plugin);
                    if (inspector != null && inspector.detailMode())
                        inspector.verifyInputFailed(gn.plugin, outs[i]);
                    continue;
                }

                if (inspector != null && inspector.detailMode())
                {
                    if (!inspector.verify((Codec) gn.plugin, fmt, null))
                        continue;
                }

                n = new GraphNode(gn, fmt, node, node.level + 1);
                candidates.addElement(n);
                foundSomething = true;
            }
        }

//        if (!foundSomething)
//        {
//            if (node.plugin == null)
//                Log.write("  no codec supports the given input.");
//            else
//                Log.write("  no codec supports the outputs from this plugin.");
//        }

        indent = oldIndent;
        Log.setIndent(indent);
        return null;
    }

    /**
     * This defines when the search ends. The "targets" array defines the nodes
     * that are to be the "end points" (leaf nodes) of the graph. With the
     * default graph builder, the targets array contains the list of sinks that
     * can potentially support the input format.
     */
    protected GraphNode findTarget(GraphNode node)
    {
        Format outs[];

        // Expand the outputs of the next node.
        if (node.plugin == null)
        {
            outs = new Format[1];
            outs[0] = node.input;
        } else
        {
            if (node.output != null)
            {
                outs = new Format[1];
                outs[0] = node.output;
            } else
            {
                outs = node.getSupportedOutputs(node.input);
                if (outs == null || outs.length == 0)
                {
                    // Log.write("Weird!  The given plugin does not support any output.");
                    return null;
                }
            }
        }

        GraphNode n;

        // Check for the list of predefined targets.
        if (targetPlugins != null
                && (n = verifyTargetPlugins(node, outs)) != null)
            return n;

        return null;
    }

    /**
     * Given a node that has failed, this function will eliminate it from the
     * candiates list and mark it in the registry as as failed node so it won't
     * be attempted again. This method is upated by hsy on 10/10/2000. If we
     * just remove the failed node from candidates and mark it in the registy,
     * we can't guarantee we could roll back to the exact/desired node/state to
     * re-start searching, since this node might have already been removed from
     * candidates. The simple/straightforward fix is to clear up candidates and
     * plugIns and start allover again.
     */
    void removeFailure(Vector candidates, GraphNode failed, Format input)
    {
        if (failed.plugin == null)
            return;

        // This is the new implementation updated by hsy on 10/10/2000
        // Here we re-start building the graph allover again. Clear
        // candidates and put the initial node into it.
        Log.comment("Failed to open plugin " + failed.plugin
                + ". Will re-build the graph allover again");
        candidates.removeAllElements();
        GraphNode hsyn = new GraphNode(null, (PlugIn) null, input, null, 0);
        indent = 1;
        Log.setIndent(indent);

        candidates.addElement(hsyn);

        // clear up the hashtable plugIns too, only let it keep all the failed
        // nodes, so that we are not going to attempt these nodes again.
        failed.failed = true;
        plugIns.put(failed.plugin.getClass().getName(), failed);

        Enumeration e = plugIns.keys();
        while (e.hasMoreElements())
        {
            String ss = e.nextElement();
            GraphNode nn = plugIns.get(ss);
            if (!nn.failed)
                plugIns.remove(ss);
        }
    }

    /**
     * Reset local cache and reuse the same instance for graph building.
     */
    public void reset()
    {
        // damencho change enum keyword to compile with 1.5
        Enumeration enum1 = plugIns.elements();
        GraphNode n;
        while (enum1.hasMoreElements())
        {
            n = enum1.nextElement();
            n.resetAttempted();
        }
    }

    protected boolean setDefaultTargetRenderer(Format in)
    {
        // Define the final targets which uses renderers.
        if (in instanceof AudioFormat)
        {
            targetPluginNames = PlugInManager.getPlugInList(new AudioFormat(
                    null, Format.NOT_SPECIFIED, Format.NOT_SPECIFIED,
                    Format.NOT_SPECIFIED, Format.NOT_SPECIFIED,
                    Format.NOT_SPECIFIED, Format.NOT_SPECIFIED,
                    Format.NOT_SPECIFIED, null), null, PlugInManager.RENDERER);
        } else if (in instanceof VideoFormat)
        {
            targetPluginNames = PlugInManager.getPlugInList(new VideoFormat(
                    null, null, Format.NOT_SPECIFIED, null,
                    Format.NOT_SPECIFIED // frameRate ???
                    ), null, PlugInManager.RENDERER);
        } else
        {
            targetPluginNames = PlugInManager.getPlugInList(null, null,
                    PlugInManager.RENDERER);
        }

        // No target available.
        if (targetPluginNames == null || targetPluginNames.size() == 0)
        {
            // Log.write("The graph builder does not recognize the input format at all:");
            // Log.write(in.toString());
            return false;
        }

        targetPlugins = new GraphNode[targetPluginNames.size()];
        targetType = PlugInManager.RENDERER;

        return true;
    }

    /**
     * Set the default targets, which are the renderers.
     */
    protected boolean setDefaultTargets(Format in)
    {
        return setDefaultTargetRenderer(in);
    }

    /**
     * Given a protential graph, verify it.
     */
    protected GraphNode verifyGraph(GraphNode node)
    {
        Format prevFormat = null;
        Vector used = new Vector(5);

        if (node.plugin == null)
        {
            // There's nothing to build.
            // i.e. the output from the source (demux) works just fine.
            // Probably just need to be multiplexed.
            return null;
        }

        Log.setIndent(indent++);

        // Build the graph from the last node.
        while (node != null && node.plugin != null)
        {
            if (used.contains(node.plugin))
            {
                // That plugin has already been used in the same path,
                // we'll need to instantiate another one of its kind.
                PlugIn p;
                if (node.cname == null
                        || (p = createPlugIn(node.cname, -1)) == null)
                {
                    Log.write("Failed to instantiate " + node.cname);
                    return node;
                }
                node.plugin = p;
            } else
            {
                used.addElement(node.plugin);
            }

            if ((node.type == -1 || node.type == PlugInManager.RENDERER)
                    && node.plugin instanceof Renderer)
            {
                ((Renderer) node.plugin).setInputFormat(node.input);
            } else if ((node.type == -1 || node.type == PlugInManager.CODEC)
                    && node.plugin instanceof Codec)
            {
                ((Codec) node.plugin).setInputFormat(node.input);
                if (prevFormat != null)
                    ((Codec) node.plugin).setOutputFormat(prevFormat);
                else if (node.output != null)
                    ((Codec) node.plugin).setOutputFormat(node.output);
            }

            // For renderers, we wait till prefetching to
            // open the device.

            if (!((node.type == -1 || node.type == PlugInManager.RENDERER) && node.plugin instanceof Renderer))
            {
                try
                {
                    node.plugin.open();
                } catch (Exception e)
                {
                    Log.warning("Failed to open: " + node.plugin);
                    node.failed = true;
                    return node;
                }
            }

            prevFormat = node.input;
            node = node.prev;
        }

        Log.setIndent(indent--);

        return null;
    }

    /**
     * Check for a match in the list of predefined targets.
     */
    protected GraphNode verifyTargetPlugins(GraphNode node, Format outs[])
    {
        GraphNode gn;
        Format fmt;

        for (int i = 0; i < targetPlugins.length; i++)
        {
            if ((gn = targetPlugins[i]) == null)
            {
                String name = (String) targetPluginNames.elementAt(i);
                if (name == null)
                    continue;

                // Initial screening before instantiating the objects.
                Format base[] = PlugInManager.getSupportedInputFormats(name,
                        targetType);
                if (matches(outs, base, null, null) == null)
                    continue;

                // Passing initial test, we'll want to instantiate it
                // to get more info from it.
                if ((gn = getPlugInNode(name, targetType, plugIns)) == null)
                {
                    targetPluginNames.setElementAt(null, i);
                    continue;
                }

                targetPlugins[i] = gn;
            }

            if ((fmt = matches(outs, gn.getSupportedInputs(), node.plugin,
                    gn.plugin)) != null)
            {
                // found the target.

                if (inspector != null)
                {
                    if (node.plugin != null
                            && !inspector.verify((Codec) node.plugin,
                                    node.input, fmt))
                        continue;
                    if ((gn.type == -1 || gn.type == PlugInManager.CODEC)
                            && gn.plugin instanceof Codec)
                    {
                        if (!inspector.verify((Codec) gn.plugin, fmt, null))
                            continue;
                    } else if ((gn.type == -1 || gn.type == PlugInManager.RENDERER)
                            && gn.plugin instanceof Renderer)
                    {
                        if (!inspector.verify((Renderer) gn.plugin, fmt))
                            continue;
                    }
                }

                return new GraphNode(gn, fmt, node, node.level + 1);
            }
        }

        return null;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy