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

gov.nasa.worldwind.layers.GARSGraticuleLayer Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2014 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration.
 * All Rights Reserved.
 */
package gov.nasa.worldwind.layers;

import gov.nasa.worldwind.View;
import gov.nasa.worldwind.avlist.AVKey;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.globes.Globe;
import gov.nasa.worldwind.render.*;
import gov.nasa.worldwind.util.Logging;

import java.awt.*;
import java.awt.geom.*;
import java.util.ArrayList;

/**
 * Displays the geographic Global Area Reference System (GARS) graticule. The graticule has four levels. The first level
 * displays lines of latitude and longitude. The second level displays 30 minute square grid cells. The third level
 * displays 15 minute grid cells. The fourth and final level displays 5 minute grid cells.
 *
 * This graticule is intended to be used on 2D globes because it is so dense.
 *
 * @version $Id: GARSGraticuleLayer.java 2384 2014-10-14 21:55:10Z tgaskins $
 */
public class GARSGraticuleLayer extends AbstractGraticuleLayer
{
    public static final String GRATICULE_GARS_LEVEL_0 = "Graticule.GARSLevel0";
    public static final String GRATICULE_GARS_LEVEL_1 = "Graticule.GARSLevel1";
    public static final String GRATICULE_GARS_LEVEL_2 = "Graticule.GARSLevel2";
    public static final String GRATICULE_GARS_LEVEL_3 = "Graticule.GARSLevel3";

    protected static final int MIN_CELL_SIZE_PIXELS = 40; // TODO: make settable

    protected GraticuleTile[][] gridTiles = new GraticuleTile[18][36]; // 10 degrees row/col
    protected ArrayList latitudeLabels = new ArrayList();
    protected ArrayList longitudeLabels = new ArrayList();
    protected String angleFormat = Angle.ANGLE_FORMAT_DMS;
    /**
     * Indicates the eye altitudes in meters below which each level should be displayed.
     */
    protected double[] thresholds = new double[] {1200e3, 600e3, 180e3}; // 30 min, 15 min, 5 min

    public GARSGraticuleLayer()
    {
        initRenderingParams();
        this.setPickEnabled(false);
        this.setName(Logging.getMessage("layers.LatLonGraticule.Name"));
    }

    /**
     * Get the graticule division and angular display format. Can be one of {@link gov.nasa.worldwind.geom.Angle#ANGLE_FORMAT_DD}
     * or {@link gov.nasa.worldwind.geom.Angle#ANGLE_FORMAT_DMS}.
     *
     * @return the graticule division and angular display format.
     */
    public String getAngleFormat()
    {
        return this.angleFormat;
    }

    /**
     * Sets the graticule division and angular display format. Can be one of {@link
     * gov.nasa.worldwind.geom.Angle#ANGLE_FORMAT_DD}, {@link gov.nasa.worldwind.geom.Angle#ANGLE_FORMAT_DMS} of {@link
     * gov.nasa.worldwind.geom.Angle#ANGLE_FORMAT_DM}.
     *
     * @param format the graticule division and angular display format.
     *
     * @throws IllegalArgumentException is format is null.
     */
    public void setAngleFormat(String format)
    {
        if (format == null)
        {
            String message = Logging.getMessage("nullValue.StringIsNull");
            Logging.logger().severe(message);
            throw new IllegalArgumentException(message);
        }

        if (this.angleFormat.equals(format))
            return;

        this.angleFormat = format;
        this.clearTiles();
        this.lastEyePoint = null; // force graticule to update
    }

    /**
     * Specifies the eye altitude below which the 30 minute grid is displayed.
     *
     * @param altitude the eye altitude in meters below which the 30 minute grid is displayed.
     */
    public void set30MinuteThreshold(double altitude)
    {
        this.thresholds[0] = altitude;
    }

    /**
     * Indicates the eye altitude below which the 30 minute grid is displayed.
     *
     * @return the eye altitude in meters below which the 30 minute grid is displayed.
     */
    public double get30MinuteThreshold()
    {
        return this.thresholds[0];
    }

    /**
     * Specifies the eye altitude below which the 15 minute grid is displayed.
     *
     * @param altitude the eye altitude in meters below which the 15 minute grid is displayed.
     */
    public void set15MinuteThreshold(double altitude)
    {
        this.thresholds[1] = altitude;
    }

    /**
     * Indicates the eye altitude below which the 15 minute grid is displayed.
     *
     * @return the eye altitude in meters below which the 15 minute grid is displayed.
     */
    public double get15MinuteThreshold()
    {
        return this.thresholds[1];
    }

    /**
     * Specifies the eye altitude below which the 5 minute grid is displayed.
     *
     * @param altitude the eye altitude in meters below which the 5 minute grid is displayed.
     */
    public void set5MinuteThreshold(double altitude)
    {
        this.thresholds[2] = altitude;
    }

    /**
     * Indicates the eye altitude below which the 5 minute grid is displayed.
     *
     * @return the eye altitude in meters below which the 5 minute grid is displayed.
     */
    public double get5MinuteThreshold()
    {
        return this.thresholds[2];
    }

    // --- Graticule Rendering --------------------------------------------------------------

    protected void initRenderingParams()
    {
        GraticuleRenderingParams params;
        // Ten degrees grid
        params = new GraticuleRenderingParams();
        params.setValue(GraticuleRenderingParams.KEY_LINE_COLOR, Color.WHITE);
        params.setValue(GraticuleRenderingParams.KEY_LABEL_COLOR, Color.WHITE);
        params.setValue(GraticuleRenderingParams.KEY_LABEL_FONT, Font.decode("Arial-Bold-16"));
        setRenderingParams(GRATICULE_GARS_LEVEL_0, params);
        // One degree
        params = new GraticuleRenderingParams();
        params.setValue(GraticuleRenderingParams.KEY_LINE_COLOR, Color.YELLOW);
        params.setValue(GraticuleRenderingParams.KEY_LABEL_COLOR, Color.YELLOW);
        params.setValue(GraticuleRenderingParams.KEY_LABEL_FONT, Font.decode("Arial-Bold-14"));
        setRenderingParams(GRATICULE_GARS_LEVEL_1, params);
        // 1/10th degree - 1/6th (10 minutes)
        params = new GraticuleRenderingParams();
        params.setValue(GraticuleRenderingParams.KEY_LINE_COLOR, Color.GREEN);
        params.setValue(GraticuleRenderingParams.KEY_LABEL_COLOR, Color.GREEN);
        setRenderingParams(GRATICULE_GARS_LEVEL_2, params);
        // 1/100th degree - 1/60th (one minutes)
        params = new GraticuleRenderingParams();
        params.setValue(GraticuleRenderingParams.KEY_LINE_COLOR, Color.CYAN);
        params.setValue(GraticuleRenderingParams.KEY_LABEL_COLOR, Color.CYAN);
        setRenderingParams(GRATICULE_GARS_LEVEL_3, params);
    }

    protected String[] getOrderedTypes()
    {
        return new String[] {
            GRATICULE_GARS_LEVEL_0,
            GRATICULE_GARS_LEVEL_1,
            GRATICULE_GARS_LEVEL_2,
            GRATICULE_GARS_LEVEL_3,
        };
    }

    protected String getTypeFor(double resolution)
    {
        if (resolution >= 10)
            return GRATICULE_GARS_LEVEL_0;
        else if (resolution >= 0.5)
            return GRATICULE_GARS_LEVEL_1;
        else if (resolution >= .25)
            return GRATICULE_GARS_LEVEL_2;
        else if (resolution >= 5.0 / 60.0)
            return GRATICULE_GARS_LEVEL_3;

        return null;
    }

    protected void clear(DrawContext dc)
    {
        super.clear(dc);
        this.latitudeLabels.clear();
        this.longitudeLabels.clear();
        this.applyTerrainConformance();
    }

    private void applyTerrainConformance()
    {
        String[] graticuleType = getOrderedTypes();
        for (String type : graticuleType)
        {
            getRenderingParams(type).setValue(
                GraticuleRenderingParams.KEY_LINE_CONFORMANCE, this.terrainConformance);
        }
    }

    /**
     * Select the visible grid elements
     *
     * @param dc the current DrawContext.
     */
    protected void selectRenderables(DrawContext dc)
    {
        ArrayList tileList = getVisibleTiles(dc);
        if (tileList.size() > 0)
        {
            for (GraticuleTile gz : tileList)
            {
                // Select tile visible elements
                gz.selectRenderables(dc);
            }
        }
    }

    protected ArrayList getVisibleTiles(DrawContext dc)
    {
        ArrayList tileList = new ArrayList();
        Sector vs = dc.getVisibleSector();
        if (vs != null)
        {
            Rectangle2D gridRectangle = getGridRectangleForSector(vs);
            if (gridRectangle != null)
            {
                for (int row = (int) gridRectangle.getY(); row <= gridRectangle.getY() + gridRectangle.getHeight();
                    row++)
                {
                    for (int col = (int) gridRectangle.getX(); col <= gridRectangle.getX() + gridRectangle.getWidth();
                        col++)
                    {
                        if (gridTiles[row][col] == null)
                            gridTiles[row][col] = new GraticuleTile(getGridSector(row, col), 20, 0);
                        if (gridTiles[row][col].isInView(dc))
                            tileList.add(gridTiles[row][col]);
                        else
                            gridTiles[row][col].clearRenderables();
                    }
                }
            }
        }
        return tileList;
    }

    private Rectangle2D getGridRectangleForSector(Sector sector)
    {
        int x1 = getGridColumn(sector.getMinLongitude().degrees);
        int x2 = getGridColumn(sector.getMaxLongitude().degrees);
        int y1 = getGridRow(sector.getMinLatitude().degrees);
        int y2 = getGridRow(sector.getMaxLatitude().degrees);
        return new Rectangle(x1, y1, x2 - x1, y2 - y1);
    }

    private Sector getGridSector(int row, int col)
    {
        int minLat = -90 + row * 10;
        int maxLat = minLat + 10;
        int minLon = -180 + col * 10;
        int maxLon = minLon + 10;
        return Sector.fromDegrees(minLat, maxLat, minLon, maxLon);
    }

    private int getGridColumn(Double longitude)
    {
        int col = (int) Math.floor((longitude + 180) / 10d);
        return Math.min(col, 35);
    }

    private int getGridRow(Double latitude)
    {
        int row = (int) Math.floor((latitude + 90) / 10d);
        return Math.min(row, 17);
    }

    protected void clearTiles()
    {
        for (int row = 0; row < 18; row++)
        {
            for (int col = 0; col < 36; col++)
            {
                if (this.gridTiles[row][col] != null)
                {
                    this.gridTiles[row][col].clearRenderables();
                    this.gridTiles[row][col] = null;
                }
            }
        }
    }

    protected String makeAngleLabel(Angle angle, double resolution)
    {
        double epsilon = .000000001;
        String label;
        if (this.getAngleFormat().equals(Angle.ANGLE_FORMAT_DMS))
        {
            if (resolution >= 1)
                label = angle.toDecimalDegreesString(0);
            else
            {
                double[] dms = angle.toDMS();
                if (dms[1] < epsilon && dms[2] < epsilon)
                    label = String.format("%4d\u00B0", (int) dms[0]);
                else if (dms[2] < epsilon)
                    label = String.format("%4d\u00B0 %2d\u2019", (int) dms[0], (int) dms[1]);
                else
                    label = angle.toDMSString();
            }
        }
        else if (this.getAngleFormat().equals(Angle.ANGLE_FORMAT_DM))
        {
            if (resolution >= 1)
                label = angle.toDecimalDegreesString(0);
            else
            {
                double[] dms = angle.toDMS();
                if (dms[1] < epsilon && dms[2] < epsilon)
                    label = String.format("%4d\u00B0", (int) dms[0]);
                else if (dms[2] < epsilon)
                    label = String.format("%4d\u00B0 %2d\u2019", (int) dms[0], (int) dms[1]);
                else
                    label = angle.toDMString();
            }
        }
        else // default to decimal degrees
        {
            if (resolution >= 1)
                label = angle.toDecimalDegreesString(0);
            else if (resolution >= .1)
                label = angle.toDecimalDegreesString(1);
            else if (resolution >= .01)
                label = angle.toDecimalDegreesString(2);
            else if (resolution >= .001)
                label = angle.toDecimalDegreesString(3);
            else
                label = angle.toDecimalDegreesString(4);
        }

        return label;
    }

    protected void addLevel0Label(double value, String labelType, String graticuleType, double resolution,
        LatLon labelOffset)
    {
        if (labelType.equals(GridElement.TYPE_LATITUDE_LABEL))
        {
            if (!graticuleType.equals(GRATICULE_GARS_LEVEL_0) || !this.latitudeLabels.contains(value))
            {

                this.latitudeLabels.add(value);
                String label = makeAngleLabel(Angle.fromDegrees(value), resolution);
                GeographicText text = new UserFacingText(label,
                    Position.fromDegrees(value, labelOffset.getLongitude().degrees, 0));
                text.setPriority(resolution * 1e6);
                this.addRenderable(text, graticuleType);
            }
        }
        else if (labelType.equals(GridElement.TYPE_LONGITUDE_LABEL))
        {
            if (!graticuleType.equals(GRATICULE_GARS_LEVEL_0) || !this.longitudeLabels.contains(value))
            {
                this.longitudeLabels.add(value);
                String label = makeAngleLabel(Angle.fromDegrees(value), resolution);
                GeographicText text = new UserFacingText(label,
                    Position.fromDegrees(labelOffset.getLatitude().degrees, value, 0));
                text.setPriority(resolution * 1e6);
                this.addRenderable(text, graticuleType);
            }
        }
    }

    protected static ArrayList latLabels = new ArrayList(360);
    protected static ArrayList lonLabels = new ArrayList(720);
    protected static String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ";
    protected static String[][] level2Labels = new String[][] {{"3", "4"}, {"1", "2"}};

    static
    {
        for (int i = 1; i <= 720; i++)
        {
            lonLabels.add(String.format("%03d", i));
        }

        for (int i = 0; i < 360; i++)
        {
            int length = chars.length();
            int i1 = i / length;
            int i2 = i % length;
            latLabels.add(String.format("%c%c", chars.charAt(i1), chars.charAt(i2)));
        }
    }

    protected String makeLabel(Sector sector, String graticuleType)
    {
        if (graticuleType.equals(GRATICULE_GARS_LEVEL_1))
        {
            int iLat = (int) ((90 + sector.getCentroid().getLatitude().degrees) * 60 / 30);
            int iLon = (int) ((180 + sector.getCentroid().getLongitude().degrees) * 60 / 30);

            return lonLabels.get(iLon) + latLabels.get(iLat);
        }
        else if (graticuleType.equals(GRATICULE_GARS_LEVEL_2))
        {
            int minutesLat = (int) ((90 + sector.getMinLatitude().degrees) * 60);
            int j = (minutesLat % 30) / 15;
            int minutesLon = (int) ((180 + sector.getMinLongitude().degrees) * 60);
            int i = (minutesLon % 30) / 15;

            return level2Labels[j][i];
        }
        else
        {
            return "";
        }
    }

    // --- Graticule tile ----------------------------------------------------------------------

    protected class GraticuleTile
    {
        private Sector sector;
        private int divisions;
        private int level;

        private ArrayList gridElements;
        private ArrayList subTiles;

        public GraticuleTile(Sector sector, int divisions, int level)
        {
            this.sector = sector;
            this.divisions = divisions;
            this.level = level;
        }

        public Extent getExtent(Globe globe, double ve)
        {
            return Sector.computeBoundingCylinder(globe, ve, this.sector);
        }

        @SuppressWarnings({"RedundantIfStatement"})
        public boolean isInView(DrawContext dc)
        {
            if (!dc.getView().getFrustumInModelCoordinates().intersects(
                this.getExtent(dc.getGlobe(), dc.getVerticalExaggeration())))
                return false;

            if (this.level != 0)
            {
                if (dc.getView().getEyePosition().getAltitude() > thresholds[this.level - 1])
                    return false;
            }

            return true;
        }

        public double getSizeInPixels(DrawContext dc)
        {
            View view = dc.getView();
            Vec4 centerPoint = getSurfacePoint(dc, this.sector.getCentroid().getLatitude(),
                this.sector.getCentroid().getLongitude());
            double distance = view.getEyePoint().distanceTo3(centerPoint);
            double tileSizeMeter = this.sector.getDeltaLatRadians() * dc.getGlobe().getRadius();
            return tileSizeMeter / view.computePixelSizeAtDistance(distance);
        }

        public void selectRenderables(DrawContext dc)
        {
            if (this.gridElements == null)
                this.createRenderables();

            String graticuleType = getTypeFor(this.sector.getDeltaLatDegrees());
            if (this.level == 0 && dc.getView().getEyePosition().getAltitude() > thresholds[0])
            {
                LatLon labelOffset = computeLabelOffset(dc);

                for (GridElement ge : this.gridElements)
                {
                    if (ge.isInView(dc))
                    {
                        // Add level zero bounding lines and labels
                        if (ge.type.equals(GridElement.TYPE_LINE_SOUTH) || ge.type.equals(GridElement.TYPE_LINE_NORTH)
                            || ge.type.equals(GridElement.TYPE_LINE_WEST))
                        {
                            addRenderable(ge.renderable, graticuleType);
                            String labelType = ge.type.equals(GridElement.TYPE_LINE_SOUTH)
                                || ge.type.equals(GridElement.TYPE_LINE_NORTH) ?
                                GridElement.TYPE_LATITUDE_LABEL : GridElement.TYPE_LONGITUDE_LABEL;
                            GARSGraticuleLayer.this.addLevel0Label(ge.value, labelType, graticuleType,
                                this.sector.getDeltaLatDegrees(), labelOffset);
                        }
                    }
                }

                if (dc.getView().getEyePosition().getAltitude() > thresholds[0])
                    return;
            }

            // Select tile grid elements
            double eyeDistance = dc.getView().getEyePosition().getAltitude();

            if (this.level == 0 && eyeDistance <= thresholds[0]
                || this.level == 1 && eyeDistance <= thresholds[1]
                || this.level == 2)
            {
                double resolution = this.sector.getDeltaLatDegrees() / this.divisions;
                graticuleType = getTypeFor(resolution);
                for (GridElement ge : this.gridElements)
                {
                    if (ge.isInView(dc))
                    {
                        addRenderable(ge.renderable, graticuleType);
                    }
                }
            }

            if (this.level == 0 && eyeDistance > thresholds[1])
                return;
            else if (this.level == 1 && eyeDistance > thresholds[2])
                return;
            else if (this.level == 2)
                return;

            // Select child elements
            if (this.subTiles == null)
                createSubTiles();
            for (GraticuleTile gt : this.subTiles)
            {
                if (gt.isInView(dc))
                {
                    gt.selectRenderables(dc);
                }
                else
                    gt.clearRenderables();
            }
        }

        public void clearRenderables()
        {
            if (this.gridElements != null)
            {
                this.gridElements.clear();
                this.gridElements = null;
            }
            if (this.subTiles != null)
            {
                for (GraticuleTile gt : this.subTiles)
                {
                    gt.clearRenderables();
                }
                this.subTiles.clear();
                this.subTiles = null;
            }
        }

        private void createSubTiles()
        {
            this.subTiles = new ArrayList();
            Sector[] sectors = this.sector.subdivide(this.divisions);
            int nextLevel = this.level + 1;
            int subDivisions = 10;
            if (nextLevel == 1)
                subDivisions = 2;
            else if (nextLevel == 2)
                subDivisions = 3;
            for (Sector s : sectors)
            {
                this.subTiles.add(new GraticuleTile(s, subDivisions, nextLevel));
            }
        }

        /** Create the grid elements */
        private void createRenderables()
        {
            this.gridElements = new ArrayList();

            double step = sector.getDeltaLatDegrees() / this.divisions;

            // Generate meridians with labels
            double lon = sector.getMinLongitude().degrees + (this.level == 0 ? 0 : step);
            while (lon < sector.getMaxLongitude().degrees - step / 2)
            {
                Angle longitude = Angle.fromDegrees(lon);
                // Meridian
                ArrayList positions = new ArrayList(2);
                positions.add(new Position(this.sector.getMinLatitude(), longitude, 0));
                positions.add(new Position(this.sector.getMaxLatitude(), longitude, 0));

                Object line = createLineRenderable(positions, AVKey.LINEAR);
                Sector sector = Sector.fromDegrees(
                    this.sector.getMinLatitude().degrees, this.sector.getMaxLatitude().degrees, lon, lon);
                String lineType = lon == this.sector.getMinLongitude().degrees ?
                    GridElement.TYPE_LINE_WEST : GridElement.TYPE_LINE;
                GridElement ge = new GridElement(sector, line, lineType);
                ge.value = lon;
                this.gridElements.add(ge);

                // Increase longitude
                lon += step;
            }

            // Generate parallels
            double lat = this.sector.getMinLatitude().degrees + (this.level == 0 ? 0 : step);
            while (lat < this.sector.getMaxLatitude().degrees - step / 2)
            {
                Angle latitude = Angle.fromDegrees(lat);
                ArrayList positions = new ArrayList(2);
                positions.add(new Position(latitude, this.sector.getMinLongitude(), 0));
                positions.add(new Position(latitude, this.sector.getMaxLongitude(), 0));

                Object line = createLineRenderable(positions, AVKey.LINEAR);
                Sector sector = Sector.fromDegrees(
                    lat, lat, this.sector.getMinLongitude().degrees, this.sector.getMaxLongitude().degrees);
                String lineType = lat == this.sector.getMinLatitude().degrees ?
                    GridElement.TYPE_LINE_SOUTH : GridElement.TYPE_LINE;
                GridElement ge = new GridElement(sector, line, lineType);
                ge.value = lat;
                this.gridElements.add(ge);

                // Increase latitude
                lat += step;
            }

            // Draw and label a parallel at the top of the graticule. The line is apparent only on 2D globes.
            if (this.sector.getMaxLatitude().equals(Angle.POS90))
            {
                ArrayList positions = new ArrayList(2);
                positions.add(new Position(Angle.POS90, this.sector.getMinLongitude(), 0));
                positions.add(new Position(Angle.POS90, this.sector.getMaxLongitude(), 0));

                Object line = createLineRenderable(positions, AVKey.LINEAR);
                Sector sector = Sector.fromDegrees(
                    90, 90, this.sector.getMinLongitude().degrees, this.sector.getMaxLongitude().degrees);
                GridElement ge = new GridElement(sector, line, GridElement.TYPE_LINE_NORTH);
                ge.value = 90;
                this.gridElements.add(ge);
            }

            double resolution = this.sector.getDeltaLatDegrees() / this.divisions;
            if (this.level == 0)
            {
                Sector[] sectors = this.sector.subdivide(20);
                for (int j = 0; j < 20; j++)
                {
                    for (int i = 0; i < 20; i++)
                    {
                        Sector sector = sectors[j * 20 + i];
                        String label = makeLabel(sector, GRATICULE_GARS_LEVEL_1);
                        addLabel(label, sectors[j * 20 + i], resolution);
                    }
                }
            }
            else if (this.level == 1)
            {
                String label = makeLabel(this.sector, GRATICULE_GARS_LEVEL_1);

                Sector[] sectors = this.sector.subdivide();
                addLabel(label + "3", sectors[0], resolution);
                addLabel(label + "4", sectors[1], resolution);
                addLabel(label + "1", sectors[2], resolution);
                addLabel(label + "2", sectors[3], resolution);
            }
            else if (this.level == 2)
            {
                String label = makeLabel(this.sector, GRATICULE_GARS_LEVEL_1);
                label += makeLabel(this.sector, GRATICULE_GARS_LEVEL_2);

                resolution = 0.26; // make label priority a little higher than level 2's
                Sector[] sectors = this.sector.subdivide(3);
                addLabel(label + "7", sectors[0], resolution);
                addLabel(label + "8", sectors[1], resolution);
                addLabel(label + "9", sectors[2], resolution);
                addLabel(label + "4", sectors[3], resolution);
                addLabel(label + "5", sectors[4], resolution);
                addLabel(label + "6", sectors[5], resolution);
                addLabel(label + "1", sectors[6], resolution);
                addLabel(label + "2", sectors[7], resolution);
                addLabel(label + "3", sectors[8], resolution);
            }
        }

        protected void addLabel(String label, Sector sector, double resolution)
        {
            GeographicText text = new UserFacingText(label, new Position(sector.getCentroid(), 0));
            text.setPriority(resolution * 1e6);
            GridElement ge = new GridElement(sector, text, GridElement.TYPE_GRIDZONE_LABEL);
            this.gridElements.add(ge);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy