public interface IGISApplication
{
    /**
     * @return A MapBase or any inherited classes or null if not created in application
     */
    MapBase getMap();

    /**
     * @return A authority for sync purposes or empty string if not sync anything
     */
    String getAuthority();

    /**
     * Add account to android account storage
     * @param name Account name (must be uniq)
     * @param url NextGIS Web Server URL
     * @param login User login
     * @param password User password
     * @param token A token returned from NextGIS Web Server (may be empty string)
     * @return true on success or false
     */
    boolean addAccount(String name, String url, String login, String password, String token);

    /**
     * Update account information
     * @param name Account name (the account must be exist)
     * @param key The account key to change (i.e. URL)
     * @param value The new value for key
     */
    void setUserData(String name, String key, String value);

    /**
     * Change password for account
     * @param name Account name (the account must be exist)
     * @param value New password
     */
    void setPassword(String name,String value);

    /**
     * @param accountName Account name
     * @return Account by its name
     */
    Account getAccount(String accountName);

    /**
     * Remove an account
     * @param account Account to remove
     * @return An @see AccountManagerFuture which resolves to a Boolean, true if the account has been successfully removed
     */
    AccountManagerFuture<Boolean> removeAccount(Account account);

    /**
     * @param account Account object
     * @return Account URL
     */
    String getAccountUrl(Account account);

    /**
     * Return some account data
     * @param account account object
     * @param key key to return
     * @return value in user key - value map
     */
    String getAccountUserData(Account account, String key);

    /**
     * @param account Account object
     * @return Account login
     */
    String getAccountLogin(Account account);

    /**
     * @param account Account object
     * @return Account password
     */
    String getAccountPassword(Account account);

    /**
     * @return A GpsEventSource or null if not needed or created in application
     */
    GpsEventSource getGpsEventSource();

    /**
     * Show settings Activity or nothing
     */
    void showSettings(String setting);

    /**
     * Send target event to analytics
     */
    void sendEvent(String category, String action, String label);

    /**
     * Send screen hit to analytics
     */
    void sendScreen(String name);

    /**
     * Get accounts authenticator type
     */
    String getAccountsType();

    /**
     * Get LayerFactory
     */
    LayerFactory getLayerFactory();
} public interface IGeometryCache {

    /**
     * Check if item with specified id is exist in cache
     * @param featureId Feature id
     * @return true if item is exist or false
     */
    boolean isItemExist(long featureId);

    /**
     * Add item to cache
     * @param id Feature identificator
     * @param envelope Envelope
     */
    IGeometryCacheItem addItem(long id, GeoEnvelope envelope);

    /**
     * Return cache item by feature identificator
     * @param featureId Feature identificator
     * @return Cache item
     */
    IGeometryCacheItem getItem(long featureId);

    /**
     * Remove item from cache
     * @param featureId Feature id of cache item
     * @return removed item or null
     */
    IGeometryCacheItem removeItem(long featureId);

    /**
     * Return count of items
     * @return count of items
     */
    int size();

    /**
     * Remove all items from cache
     */
    void clear();

    /**
     * Search items intersected provided envelope
     * @param extent Envelope to search
     * @return List of items intersected provided envelope
     */
    List<IGeometryCacheItem> search(GeoEnvelope extent);

    /**
     * Get all items
     * @return List of all items
     */
    List<IGeometryCacheItem> getAll();

    void changeId(long oldFeatureId, long newFeatureId);

    void save(File path);

    void load(File path);
} public interface IGeometryCacheItem {
    /**
     *
     * @return Return an envelope
     */
    GeoEnvelope getEnvelope();

    /**
     *
     * @return A feature identificator connected with this cache item
     */
    long getFeatureId();


    /**
     * Set a eature identificator connected with this cache item
     * @param id Feature identificator
     */
    void setFeatureId(long id);
} public interface IJSONStore
{
    /**
     * Store object in json
     * @return json object with stored data
     * @throws JSONException
     */
    JSONObject toJSON()
            throws JSONException;

    /**
     * Restore object from json
     * @param jsonObject where the stored data are
     * @throws JSONException
     */
    void fromJSON(JSONObject jsonObject)
            throws JSONException;
} public interface IProgressor {
    /**
     * Set maximum value of progress
     * @param maxValue maximum value
     */
    void setMax(int maxValue);

    /**
     * The process should execute this method if it should be canceled.
     * @return true if progress must be canceled, or false
     */
    boolean isCanceled();

    /**
     * Set current progress.
     * @param value current progress
     */
    void setValue(int value);

    /**
     * If progress cannot determinate the max value and current value, it can be set as indeterminate.
     * @param indeterminate true to set indeterminate state, or false
     */
    void setIndeterminate(boolean indeterminate);

    /**
     * Set some message to show in progress dialog or somewhere else.
     * @param message a message
     */
    void setMessage(String message);
} public interface IStyleRule
{
    /**
     * Set style parameters
     * @param style New style
     * @param featureId The feature identifier to set style
     */
    void setStyleParams(
            Style style,
            long featureId);
} public class Feature
        implements IJSONStore
{
    protected long                    mId;
    protected GeoGeometry             mGeometry;
    protected List<Object>            mFieldValues;
    protected List<Field>             mFields;
    protected Map<String, AttachItem> mAttachments;


    public Feature()
    {
        mId = Constants.NOT_FOUND;
        mFields = new ArrayList<>();
        mFieldValues = new ArrayList<>();
        mAttachments = new HashMap<>();
    }


    public Feature(
            long id,
            List<Field> fields)
    {
        mId = id;
        mFields = fields;
        mFieldValues = new ArrayList<>(fields.size());
        mAttachments = new HashMap<>();
    }


    public Feature(Feature other)
    {
        mId = other.getId();
        mGeometry = other.getGeometry();
        mFields = other.getFields();
        mFieldValues = other.getFieldValues();
        mAttachments = other.getAttachments();
    }


    public long getId()
    {
        return mId;
    }


    public void setId(long id)
    {
        mId = id;
    }


    public void setGeometry(GeoGeometry geometry)
    {
        mGeometry = geometry;
    }


    public GeoGeometry getGeometry()
    {
        return mGeometry;
    }


    public boolean setFieldValue(
            int index,
            Object value)
    {
        if (index < 0 || index >= mFields.size()) {
            return false;
        }
        if (mFieldValues.size() <= index) {
            for (int i = mFieldValues.size(); i <= index; i++) {
                mFieldValues.add(null);
            }
        }
        mFieldValues.set(index, value);
        return true;
    }


    public boolean setFieldValue(
            String fieldName,
            Object value)
    {
        int index = getFieldValueIndex(fieldName);
        return setFieldValue(index, value);
    }


    public boolean isValuePresent(int index)
    {
        return getFieldValue(index) != null;
    }


    public Object getFieldValue(int index)
    {
        if (mFields.isEmpty() || mFieldValues.isEmpty() || index < 0 || index >= mFields.size() ||
                index >= mFieldValues.size()) {
            return null;
        }
        return mFieldValues.get(index);
    }


    public Object getFieldValue(String fieldName)
    {
        int index = getFieldValueIndex(fieldName);
        return getFieldValue(index);
    }


    public int getFieldValueIndex(String fieldName)
    {
        for (int i = 0; i < mFields.size(); i++) {
            if (mFields.get(i).getName().equals(fieldName)) {
                return i;
            }
        }
        return NOT_FOUND;
    }


    public int getFieldValueAsInteger(String fieldName)
    {
        int index = getFieldValueIndex(fieldName);
        return getFieldValueAsInteger(index);
    }


    public int getFieldValueAsInteger(int index)
    {
        if (mFields.isEmpty() || mFieldValues.isEmpty() || index < 0 || index >= mFields.size() ||
                index >= mFieldValues.size()) {
            return Integer.MAX_VALUE;
        }

        Object val = mFieldValues.get(index);
        if (null == val) {
            return Integer.MAX_VALUE;
        }

        if (val instanceof Long) {
            Long lval = (Long) val;
            return lval.intValue();
        } else if (val instanceof Integer) {
            return (int) val;
        } else {
            return Integer.MAX_VALUE;
        }
    }


    public String getFieldValueAsString(String fieldName)
    {
        int index = getFieldValueIndex(fieldName);
        return getFieldValueAsString(index);
    }


    public String getFieldValueAsString(int index)
    {
        if (mFields.isEmpty() || mFieldValues.isEmpty() || index < 0 || index >= mFields.size() ||
                index >= mFieldValues.size()) {
            return "";
        }
        Object val = mFieldValues.get(index);
        if (null == val) {
            return "";
        }
        switch (mFields.get(index).getType()) {
            case FTString:
            case FTReal:
            case FTInteger:
                return val.toString();
            case FTDate:
                if (val instanceof Long) {
                    DateFormat dateFormat = DateFormat.getDateInstance();
                    return dateFormat.format(new Date((Long) val));
                }
                break;
            case FTTime:
                if (val instanceof Long) {
                    DateFormat dateFormat = DateFormat.getTimeInstance();
                    return dateFormat.format(new Date((Long) val));
                }
                break;
            case FTDateTime:
                if (val instanceof Long) {
                    DateFormat dateFormat = DateFormat.getDateTimeInstance();
                    return dateFormat.format(new Date((Long) val));
                }
                break;
        }
        return "";
    }


    @Override
    public JSONObject toJSON()
            throws JSONException
    {
        JSONObject oJSONOut = new JSONObject();
        oJSONOut.put(GEOJSON_TYPE, GEOJSON_TYPE_Feature);
        oJSONOut.put(GEOJSON_FEATURE_ID, mId);
        oJSONOut.put(GEOJSON_GEOMETRY, mGeometry.toJSON());
        JSONObject oJSONProp = new JSONObject();
        for (int i = 0; i < mFieldValues.size(); i++) {
            String key = mFields.get(i).getName();
            oJSONProp.put(key, mFieldValues.get(i));
        }
        oJSONOut.put(GEOJSON_PROPERTIES, oJSONProp);
        return oJSONOut;
    }


    @Override
    public void fromJSON(JSONObject jsonObject)
            throws JSONException
    {
        if (!jsonObject.getString(GEOJSON_TYPE).equals(GEOJSON_TYPE_Feature)) {
            throw new JSONException("not valid geojson feature");
        }
        mId = jsonObject.getInt(GEOJSON_FEATURE_ID);
        JSONObject oJSONGeom = jsonObject.getJSONObject(GEOJSON_GEOMETRY);
        mGeometry = GeoGeometryFactory.fromJson(oJSONGeom);
        JSONObject jsonAttributes = jsonObject.getJSONObject(GEOJSON_PROPERTIES);
        Iterator<String> iter = jsonAttributes.keys();
        while (iter.hasNext()) {
            String key = iter.next();
            Object value = jsonAttributes.get(key);
            mFieldValues.add(value);
        }
    }


    public List<Field> getFields()
    {
        return mFields;
    }


    public List<Object> getFieldValues()
    {
        return mFieldValues;
    }


    public ContentValues getContentValues(boolean withId)
    {
        ContentValues values = new ContentValues();
        if (withId) {
            if (mId != Constants.NOT_FOUND) {
                values.put(Constants.FIELD_ID, mId);
            } else {
                values.putNull(Constants.FIELD_ID);
            }
        }

        try {
            if (null != mGeometry) {
                values.put(FIELD_GEOM, mGeometry.toBlob());
            }
        } catch (IOException e) { //if exception - not create geom
            e.printStackTrace();
        }
        if (!values.containsKey(FIELD_GEOM)) {
            values.putNull(FIELD_GEOM);
        }

        for (int i = 0; i < mFields.size(); i++) {
            Field field = mFields.get(i);

            if (!isValuePresent(i)) {
                values.putNull(field.getName());
                continue;
            }

            switch (field.getType()) {
                case FTString:
                    values.put(field.getName(), getFieldValueAsString(i));
                    break;

                case FTInteger:
                    Object intVal = getFieldValue(i);
                    if (intVal instanceof Integer) {
                        values.put(field.getName(), (int) intVal);
                    } else if (intVal instanceof Long) {
                        values.put(field.getName(), (long) intVal);
                    } else {
                        Log.d(TAG, "skip value: " + intVal.toString());
                    }
                    break;

                case FTReal:
                    Object realVal = getFieldValue(i);
                    if (realVal instanceof Double) {
                        values.put(field.getName(), (double) realVal);
                    } else if (realVal instanceof Float) {
                        values.put(field.getName(), (float) realVal);
                    } else {
                        Log.d(TAG, "skip value: " + realVal.toString());
                    }
                    break;

                case FTDate:
                case FTTime:
                case FTDateTime:
                    Object dateVal = getFieldValue(i);
                    if (dateVal instanceof Date) {
                        Date date = (Date) dateVal;
                        values.put(field.getName(), date.getTime());
                    } else if (dateVal instanceof Long) {
                        values.put(field.getName(), (long) dateVal);
                    } else if (dateVal instanceof Calendar) {
                        Calendar cal = (Calendar) dateVal;
                        values.put(field.getName(), cal.getTimeInMillis());
                    } else {
                        Log.d(TAG, "skip value: " + dateVal.toString());
                    }
                    break;
            }
        }

        return values;
    }


    public void fromCursor(Cursor cursor)
    {
        if (null == cursor) {
            return;
        }
        mId = cursor.getLong(cursor.getColumnIndex(FIELD_ID));

        try {
            mGeometry =
                    GeoGeometryFactory.fromBlob(cursor.getBlob(cursor.getColumnIndex(FIELD_GEOM)));
        } catch (IOException e) { //let it be empty geometry
            e.printStackTrace();
        }

        for (int i = 0; i < mFields.size(); i++) {
            Field field = mFields.get(i);
            int index = cursor.getColumnIndex(field.getName());
            if (cursor.isNull(index)) {
                setFieldValue(i, null);
            } else {
                if (index != NOT_FOUND) {
                    switch (field.getType()) {
                        case FTString:
                            setFieldValue(i, cursor.getString(index));
                            break;
                        case FTInteger:
                            setFieldValue(i, cursor.getLong(index));
                            break;
                        case FTReal:
                            setFieldValue(i, cursor.getDouble(index));
                            break;
                        case FTDate:
                        case FTTime:
                        case FTDateTime:
                            TimeZone timeZone = TimeZone.getDefault();
                            timeZone.setRawOffset(0); // set to UTC
                            Calendar calendar = Calendar.getInstance(timeZone);
                            calendar.setTimeInMillis(cursor.getLong(index));
                            setFieldValue(i, calendar.getTimeInMillis());
                            break;
                        default:
                            break;
                    }
                }
            }
        }
    }


    public boolean equalsData(Feature f)
    {
        if (null == f) {
            return false;
        }
        //compare attributes
        Log.d(TAG, "Feature id:" + mId + " compare attributes");
        for (int i = 0; i < mFields.size(); i++) {
            Field field = mFields.get(i);

            Object value = getFieldValue(i);
            Object valueOther = f.getFieldValue(field.getName());

            //Log.d(TAG, value + "<->" + valueOther);

            if (null == value) {
                if (null != valueOther) {
                    if (field.getType() == FTDateTime) {
                        long lValue = dateObjectToLong(valueOther);
                        if (lValue > 0) {
                            Log.d(TAG, value + "<->" + valueOther);
                            return false;
                        }
                    } else {
                        Log.d(TAG, value + "<->" + valueOther);
                        return false;
                    }
                }
            } else if (null == valueOther) {
                Log.d(TAG, value + "<->" + valueOther);
                return false;
            } else {
                if (field.getType() == GeoConstants.FTInteger) {
                    if (!checkIntegerEqual(value, valueOther)) {
                        Log.d(TAG, value + "<->" + valueOther);
                        return false;
                    }
                } else if (field.getType() == GeoConstants.FTReal) {
                    if (!checkRealEqual(value, valueOther)) {
                        Log.d(TAG, value + "<->" + valueOther);
                        return false;
                    }
                } else if (field.getType() == GeoConstants.FTDate ||
                        field.getType() == GeoConstants.FTTime ||
                        field.getType() == GeoConstants.FTDateTime) {
                    if (!checkDateEqual(value, valueOther)) {
                        Log.d(TAG, value + "<->" + valueOther);
                        return false;
                    }
                } else if (!value.equals(valueOther)) { // any other cases
                    Log.d(TAG, value + "<->" + valueOther);
                    return false;
                }
            }
        }

        //compare geometry
        Log.d(TAG, "Feature id:" + mId + " compare geometry");
        if (null == mGeometry) {
            return null == f.getGeometry();
        }
        return mGeometry.equals(f.getGeometry());
    }


    private long dateObjectToLong(Object value)
    {
        if (value instanceof Long) {
            return (Long) value;
        } else if (value instanceof Date) {
            Date date = (Date) value;
            return date.getTime();
        } else if (value instanceof Calendar) {
            Calendar cal = (Calendar) value;
            return cal.getTimeInMillis();
        }

        return Constants.NOT_FOUND;
    }


    private boolean checkDateEqual(
            Object value,
            Object valueOther)
    {
        return dateObjectToLong(value) == dateObjectToLong(valueOther);
    }


    private boolean checkRealEqual(
            Object value,
            Object valueOther)
    {
        if (value instanceof Float && valueOther instanceof Double) {
            Float vlong = (Float) value;
            Double ovlong = (Double) valueOther;
            return vlong == ovlong.floatValue();
        } else if (value instanceof Double && valueOther instanceof Float) {
            Double vlong = (Double) value;
            Float ovlong = (Float) valueOther;
            return vlong.floatValue() == ovlong;
        } else {
            return value.equals(valueOther);
        }
    }


    private boolean checkIntegerEqual(
            Object value,
            Object valueOther)
    {
        if (value instanceof Integer && valueOther instanceof Long) {
            Integer vlong = (Integer) value;
            Long ovlong = (Long) valueOther;
            return vlong == ovlong.intValue();
        } else if (value instanceof Long && valueOther instanceof Integer) {
            Long vlong = (Long) value;
            Integer ovlong = (Integer) valueOther;
            return vlong.intValue() == ovlong;
        } else {
            return value.equals(valueOther);
        }
    }


    public boolean equalsAttachments(Feature f)
    {
        if (null == f) {
            return false;
        }
        //compare attachments
        Map<String, AttachItem> attachments = f.getAttachments();
        if (mAttachments.size() != attachments.size()) {
            return false;
        }
        for (AttachItem item : mAttachments.values()) {
            AttachItem otherItem = attachments.get(item.getAttachId());
            if (null == otherItem) {
                return false;
            }
            if (!item.equals(otherItem)) {
                return false;
            }
        }
        return true;
    }


    @Override
    public boolean equals(Object o)
    {
        if (super.equals(o)) //if same pointers
        {
            return true;
        }

        Feature other = (Feature) o;
        // go deeper
        return equalsData(other) && equalsAttachments(other);
    }


    public void addAttachment(AttachItem item)
    {
        mAttachments.put(item.getAttachId(), item);
    }


    public void addAttachments(Map<String, AttachItem> attachments)
    {
        if (null == attachments) {
            return;
        }
        mAttachments = attachments;
    }


    public Map<String, AttachItem> getAttachments()
    {
        return mAttachments;
    }


    public void clearAttachments()
    {
        mAttachments.clear();
    }


    public long getMaxAttachId()
    {
        long maxAttachId = 0;

        if (null == mAttachments) {
            return maxAttachId;
        }

        for (String key : mAttachments.keySet()) {
            long keyL = Long.parseLong(key);
            maxAttachId = Math.max(maxAttachId, keyL);
        }

        return maxAttachId;
    }
} public class Field implements IJSONStore, Parcelable {
    protected int mType;
    protected String mName;
    protected String mAlias;

    protected static final String JSON_ALIAS_KEY = "alias";

    public Field() { }

    public Field(int type, String name, String alias) {
        mType = type;
        mName = name;
        if (TextUtils.isEmpty(alias)) {
            mAlias = mName;
        } else {
            mAlias = alias;
        }
    }

    private Field(Parcel in) {
        this();
        mType = in.readInt();
        mName = in.readString();
        mAlias = in.readString();
    }

    @Override
    public JSONObject toJSON() throws JSONException {
        JSONObject rootObject = new JSONObject();
        rootObject.put(Constants.JSON_TYPE_KEY, mType);
        rootObject.put(Constants.JSON_NAME_KEY, mName);
        rootObject.put(JSON_ALIAS_KEY, mAlias);
        return rootObject;
    }

    @Override
    public void fromJSON(JSONObject jsonObject) throws JSONException {
        mType = jsonObject.getInt(Constants.JSON_TYPE_KEY);
        mName = jsonObject.getString(Constants.JSON_NAME_KEY);
        if (jsonObject.has(JSON_ALIAS_KEY)) {
            mAlias = jsonObject.getString(JSON_ALIAS_KEY);
        }
    }

    public int getType() {
        return mType;
    }

    public String getName() {
        return mName;
    }

    public String getAlias() {
        return mAlias;
    }

    public void setName(String name) {
        mName = name;
    }

    public void setAlias(String alias) {
        mAlias = alias;
    }

    public static final Creator<Field> CREATOR = new Creator<Field>() {
        @Override
        public Field createFromParcel(Parcel in) {
            return new Field(in);
        }

        @Override
        public Field[] newArray(int size) {
            return new Field[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeInt(mType);
        parcel.writeString(mName);
        parcel.writeString(mAlias);
    }
} public class GeoEnvelope
        implements IJSONStore
{
    protected Double mMinX;
    protected Double mMaxX;
    protected Double mMinY;
    protected Double mMaxY;

    public final static int enumGISPtPosLeft = 1;
    public final static int enumGISPtPosRight = 2;
    public final static int enumGISPtPosBottom = 3;
    public final static int enumGISPtPosTop = 4;

    public GeoEnvelope()
    {
        unInit();
    }


    public void unInit()
    {
        mMinX = null;
        mMaxX = null;
        mMinY = null;
        mMaxY = null;
    }


    public GeoEnvelope(
            double minX,
            double maxX,
            double minY,
            double maxY)
    {
        mMinX = minX;
        mMaxX = maxX;
        mMinY = minY;
        mMaxY = maxY;
    }


    public GeoEnvelope(final GeoEnvelope env)
    {
        mMinX = env.mMinX;
        mMaxX = env.mMaxX;
        mMinY = env.mMinY;
        mMaxY = env.mMaxY;
    }


    public void setMin(
            double x,
            double y)
    {
        mMinX = x;
        mMinY = y;
    }


    public void setMax(
            double x,
            double y)
    {
        mMaxX = x;
        mMaxY = y;
    }


    public final GeoPoint getCenter()
    {
        double x = mMinX + width() / 2.0;
        double y = mMinY + height() / 2.0;
        return new GeoPoint(x, y);
    }

    public final double getArea(){
        return width() * height();
    }


    public final double width()
    {
        return mMaxX - mMinX;
    }


    public final double height()
    {
        return mMaxY - mMinY;
    }


    public void adjust(double ratio)
    {
        double w = width() / 2.0;
        double h = height() / 2.0;
        double centerX = mMinX + w;
        double centerY = mMinY + h;

        double envRatio = w / h;

        if (envRatio == ratio) {
            return;
        }

        if (ratio > envRatio) //increase width
        {
            w = h * ratio;
            mMaxX = centerX + w;
            mMinX = centerX - w;
        } else                //increase height
        {
            h = w / ratio;
            mMaxY = centerY + h;
            mMinY = centerY - h;
        }
    }


    public void merge(final GeoEnvelope other) {
        try {
            if (isInit() && other.isInit()) {
                mMinX = Math.min(mMinX, other.mMinX);
                mMaxX = Math.max(mMaxX, other.mMaxX);
                mMinY = Math.min(mMinY, other.mMinY);
                mMaxY = Math.max(mMaxY, other.mMaxY);
            } else {
                mMinX = other.mMinX;
                mMaxX = other.mMaxX;
                mMinY = other.mMinY;
                mMaxY = other.mMaxY;
            }
        } catch (Exception ignored) {}
    }


    public final boolean isInit()
    {
        return mMinX != null && mMinY != null && mMaxX != null && mMaxY != null;
    }


    public void merge(
            double dfX,
            double dfY)
    {
        if (isInit()) {
            mMinX = Math.min(mMinX, dfX);
            mMaxX = Math.max(mMaxX, dfX);
            mMinY = Math.min(mMinY, dfY);
            mMaxY = Math.max(mMaxY, dfY);
        } else {
            mMinX = mMaxX = dfX;
            mMinY = mMaxY = dfY;
        }
    }


    public void intersect(final GeoEnvelope other)
    {
        if (intersects(other)) {
            if (isInit()) {
                mMinX = Math.max(mMinX, other.mMinX);
                mMaxX = Math.min(mMaxX, other.mMaxX);
                mMinY = Math.max(mMinY, other.mMinY);
                mMaxY = Math.min(mMaxY, other.mMaxY);
            } else {
                mMinX = other.mMinX;
                mMaxX = other.mMaxX;
                mMinY = other.mMinY;
                mMaxY = other.mMaxY;
            }
        } else {
            unInit();
        }
    }


    public final boolean intersects(final GeoEnvelope other)
    {
        return mMinX <= other.mMaxX && mMaxX >= other.mMinX &&
               mMinY <= other.mMaxY && mMaxY >= other.mMinY;
    }


    public final boolean contains(final GeoEnvelope other)
    {
        return mMinX <= other.mMinX && mMinY <= other.mMinY &&
               mMaxX >= other.mMaxX && mMaxY >= other.mMaxY;
    }


    public final boolean contains(final GeoPoint pt)
    {
        return mMinX <= pt.getX() && mMinY <= pt.getY() && mMaxX >= pt.getX() && mMaxY >= pt.getY();
    }


    public void offset(
            double x,
            double y)
    {
        mMinX += x;
        mMaxX += x;
        mMinY += y;
        mMaxY += y;
    }


    public void scale(double scale)
    {
        mMaxX = mMinX + width() * scale;
        mMaxY = mMinY + height() * scale;
    }


    public void fix()
    {
        if (mMinX > mMaxX) {
            double tmp = mMinX;
            mMinX = mMaxX;
            mMaxX = tmp;
        }

        if (mMinY > mMaxY) {
            double tmp = mMinY;
            mMinY = mMaxY;
            mMaxY = tmp;
        }
    }


    public String toString()
    {
        return "MinX: " + mMinX + ", MinY: " + mMinY + ", MaxX: " + mMaxX + ", MaxY: " + mMaxY;
    }


    @Override
    public JSONObject toJSON()
            throws JSONException
    {
        JSONObject oJSONBBox = new JSONObject();
        oJSONBBox.put(JSON_BBOX_MINX_KEY, getMinX());
        oJSONBBox.put(JSON_BBOX_MINY_KEY, getMinY());
        oJSONBBox.put(JSON_BBOX_MAXX_KEY, getMaxX());
        oJSONBBox.put(JSON_BBOX_MAXY_KEY, getMaxY());
        return oJSONBBox;
    }


    public final double getMinX()
    {
        return mMinX;
    }


    public void setMinX(double x)
    {
        mMinX = x;
    }


    public final double getMinY()
    {
        return mMinY;
    }


    public void setMinY(double y)
    {
        mMinY = y;
    }


    public final double getMaxX()
    {
        return mMaxX;
    }


    public void setMaxX(double x)
    {
        mMaxX = x;
    }


    public final double getMaxY()
    {
        return mMaxY;
    }


    public void setMaxY(double y)
    {
        mMaxY = y;
    }


    @Override
    public void fromJSON(JSONObject jsonObject)
            throws JSONException
    {
        setMinX(jsonObject.getDouble(JSON_BBOX_MINX_KEY));
        setMinY(jsonObject.getDouble(JSON_BBOX_MINY_KEY));
        setMaxX(jsonObject.getDouble(JSON_BBOX_MAXX_KEY));
        setMaxY(jsonObject.getDouble(JSON_BBOX_MAXY_KEY));
    }

    public void set(GeoEnvelope env) {
        if (env == null)
            return;

        mMinX = env.mMinX;
        mMaxX = env.mMaxX;
        mMinY = env.mMinY;
        mMaxY = env.mMaxY;
    }


    /**
     * Sutherland-Hodgman Polygon Clipping
     * @param pt Test point
     * @param nPos Test type
     * @return true if point inside envelope or false
     */
    public boolean isInside(final GeoPoint pt, int nPos) {
        switch(nPos)
        {
            case enumGISPtPosLeft://XMin
                return (pt.getX() > mMinX);
            case enumGISPtPosRight://XMax
                return (pt.getX() < mMaxX);
            case enumGISPtPosBottom://YMin
                return (pt.getY() < mMaxY);
            case enumGISPtPosTop://YMax
                return (pt.getY() > mMinY);
        }
        return false;
    }
} public abstract class GeoGeometry
        implements Serializable
{
    protected static final long serialVersionUID = -1241179697270831761L;
    protected int mCRS;


    public boolean project(int toCrs)
    {
        return (mCRS == CRS_WGS84 && toCrs == CRS_WEB_MERCATOR ||
                mCRS == CRS_WEB_MERCATOR && toCrs == CRS_WGS84) && rawProject(toCrs);
    }


    protected boolean rawProject(int toCrs)
    {
        mCRS = toCrs;
        return true;
    }


    public abstract GeoEnvelope getEnvelope();


    public void setCRS(int crs)
    {
        mCRS = crs;
    }


    public int getCRS()
    {
        return mCRS;
    }


    public JSONObject toJSON()
            throws JSONException
    {
        JSONObject jsonOutObject = new JSONObject();
        jsonOutObject.put(GEOJSON_TYPE, typeToJSON());
        jsonOutObject.put(GEOJSON_COORDINATES, coordinatesToJSON());

        return jsonOutObject;
    }


    public String typeToJSON()
    {
        switch (getType()) {
            case GTPoint:
                return GEOJSON_TYPE_Point;
            case GTLineString:
                return GEOJSON_TYPE_LineString;
            case GTPolygon:
                return GEOJSON_TYPE_Polygon;
            case GTMultiPoint:
                return GEOJSON_TYPE_MultiPoint;
            case GTMultiLineString:
                return GEOJSON_TYPE_MultiLineString;
            case GTMultiPolygon:
                return GEOJSON_TYPE_MultiPolygon;
            case GTGeometryCollection:
                return GEOJSON_TYPE_GeometryCollection;
            case GTNone:
            default:
                return "";
        }
    }


    public abstract JSONArray coordinatesToJSON()
            throws JSONException, ClassCastException;


    public abstract int getType();


    public abstract void setCoordinatesFromJSON(JSONArray coordinates)
            throws JSONException;

    public abstract void setCoordinatesFromJSONStream(JsonReader reader, int crs)
            throws IOException;

    public abstract void setCoordinatesFromWKT(String wkt, int crs);


    public byte[] toBlobOld()
            throws IOException
    {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream os = new ObjectOutputStream(out);
        os.writeObject(this);
        return out.toByteArray();
    }


    public byte[] toBlob()
            throws IOException
    {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        DataOutputStream dataOutputStream = new DataOutputStream(out);
        write(dataOutputStream);
        return out.toByteArray();
    }


    public abstract String toWKT(boolean full);


    public boolean equals(Object o)
    {
        if (super.equals(o)) {
            return true;
        }
        GeoGeometry other = (GeoGeometry) o;
        return null != other && getType() == other.getType() && mCRS == other.getCRS();
    }


    public boolean intersects(GeoEnvelope envelope)
    {
        return getEnvelope().intersects(envelope);
    }


    /**
     * Make deep copy of geometry
     *
     * @return The geometry copy
     */
    public abstract GeoGeometry copy();

    /**
     * remove all points from geometry
     */
    public abstract void clear();

    public abstract GeoGeometry simplify(double tolerance);

    public abstract GeoGeometry clip(GeoEnvelope envelope);

    public void write(DataOutputStream stream) throws IOException{
        stream.writeInt(getType());
        stream.writeInt(mCRS);
    }

    public void read(DataInputStream stream) throws IOException{
        mCRS = stream.readInt();
    }

    public abstract boolean isValid();

    public abstract double distance(GeoGeometry geometry);
} public class GeoGeometryFactory
{
    public static GeoGeometry fromJsonStream(JsonReader reader, int crs) throws IOException {
        GeoGeometry geometry = null;
        reader.beginObject();
        while (reader.hasNext()) {
            String name = reader.nextName();
            if(name.equalsIgnoreCase(GeoConstants.GEOJSON_TYPE)){
                int type = typeFromString(reader.nextString());
                switch (type) {
                    case GTPoint:
                        geometry = new GeoPoint();
                        break;
                    case GTLineString:
                        geometry = new GeoLineString();
                        break;
                    case GTPolygon:
                        geometry = new GeoPolygon();
                        break;
                    case GTMultiPoint:
                        geometry = new GeoMultiPoint();
                        break;
                    case GTMultiLineString:
                        geometry = new GeoMultiLineString();
                        break;
                    case GTMultiPolygon:
                        geometry = new GeoMultiPolygon();
                        break;
                    case GTGeometryCollection:
                        geometry = new GeoGeometryCollection();
                    case GTNone:
                    default:
                        break;
                }
            } else if(name.equalsIgnoreCase(GeoConstants.GEOJSON_COORDINATES)) {
                if (geometry == null)
                    reader.skipValue();
                else
                    geometry.setCoordinatesFromJSONStream(reader, crs);
            } else {
                reader.skipValue();
            }
        }
        reader.endObject();

        if (geometry != null) {
            geometry.setCRS(crs);
            if (!geometry.isValid())
                return null;
        }

        return geometry;
    }

    public static GeoGeometry fromJson(JSONObject jsonObject)
            throws JSONException
    {
        String jsonType = jsonObject.getString(GEOJSON_TYPE);
        int type = typeFromString(jsonType);

        GeoGeometry output = null;
        switch (type) {
            case GTPoint:
                output = new GeoPoint();
                break;
            case GTLineString:
                output = new GeoLineString();
                break;
            case GTPolygon:
                output = new GeoPolygon();
                break;
            case GTMultiPoint:
                output = new GeoMultiPoint();
                break;
            case GTMultiLineString:
                output = new GeoMultiLineString();
                break;
            case GTMultiPolygon:
                output = new GeoMultiPolygon();
                break;
            case GTGeometryCollection:
                output = new GeoGeometryCollection();
            case GTNone:
            default:
                break;
        }

        switch (type) {
            case GTPoint:
            case GTLineString:
            case GTPolygon:
            case GTMultiPoint:
            case GTMultiLineString:
            case GTMultiPolygon:
                JSONArray coordinates = jsonObject.getJSONArray(GEOJSON_COORDINATES);
                output.setCoordinatesFromJSON(coordinates);
                break;
            case GTGeometryCollection:
                JSONArray jsonGeometries = jsonObject.getJSONArray(GEOJSON_GEOMETRIES);
                output.setCoordinatesFromJSON(jsonGeometries);
                break;
            case GTNone:
            default:
                break;
        }

        return output;
    }


    public static int typeFromString(String jsonType)
    {
        switch (jsonType) {
            case "POINT":
            case GEOJSON_TYPE_Point:
                return GTPoint;

            case "LINESTRING":
            case GEOJSON_TYPE_LineString:
                return GTLineString;

            case "POLYGON":
            case GEOJSON_TYPE_Polygon:
                return GTPolygon;

            case "MULTIPOINT":
            case GEOJSON_TYPE_MultiPoint:
                return GTMultiPoint;

            case "MULTILINESTRING":
            case GEOJSON_TYPE_MultiLineString:
                return GTMultiLineString;

            case "MULTIPOLYGON":
            case GEOJSON_TYPE_MultiPolygon:
                return GTMultiPolygon;

            case "GEOMETRYCOLLECTION":
            case GEOJSON_TYPE_GeometryCollection:
                return GTGeometryCollection;

            default:
                return GTNone;
        }
    }

    public static String typeToString(int type) {
        switch (type) {
            case GTPoint:
                return "POINT";

            case GTLineString:
                return "LINESTRING";

            case GTPolygon:
                return "POLYGON";

            case GTMultiPoint:
                return "MULTIPOINT";

            case GTMultiLineString:
                return "MULTILINESTRING";

            case GTMultiPolygon:
                return "MULTIPOLYGON";

            case GTGeometryCollection:
                return "GEOMETRYCOLLECTION";

            default:
                return "";
        }
    }

    public static GeoGeometry fromBlobOld(byte[] raw)
            throws IOException, ClassNotFoundException
    {
        if (null == raw) {
            return null;
        }
        ByteArrayInputStream in = new ByteArrayInputStream(raw);
        ObjectInputStream is = new ObjectInputStream(in);
        return (GeoGeometry) is.readObject();
    }

    public static GeoGeometry fromBlob(byte[] raw)
            throws IOException
    {
        if (null == raw) {
            return null;
        }
        ByteArrayInputStream in = new ByteArrayInputStream(raw);
        DataInputStream dataInputStream = new DataInputStream(in);
        return fromDataStream(dataInputStream);
    }


    public static GeoGeometry fromWKT(String wkt, int crs)
    {
        GeoGeometry output = null;
        wkt = wkt.trim();
        if (wkt.startsWith("POINT")) {
            output = new GeoPoint();
            output.setCoordinatesFromWKT(wkt.substring(5).trim(), crs);
        } else if (wkt.startsWith("LINESTRING")) {
            output = new GeoLineString();
            output.setCoordinatesFromWKT(wkt.substring(10).trim(), crs);
        } else if (wkt.startsWith("LINEARRING")) {
            output = new GeoLinearRing();
            output.setCoordinatesFromWKT(wkt.substring(10).trim(), crs);
        } else if (wkt.startsWith("POLYGON")) {
            output = new GeoPolygon();
            output.setCoordinatesFromWKT(wkt.substring(8, wkt.length() - 1).trim(), crs);
        } else if (wkt.startsWith("MULTIPOINT")) {
            output = new GeoMultiPoint();
            output.setCoordinatesFromWKT(wkt.substring(10).trim(), crs);
        } else if (wkt.startsWith("MULTILINESTRING")) {
            output = new GeoMultiLineString();
            output.setCoordinatesFromWKT(wkt.substring(15).trim(), crs);
        } else if (wkt.startsWith("MULTIPOLYGON")) {
            output = new GeoMultiPolygon();
            output.setCoordinatesFromWKT(wkt.substring(12).trim(), crs);
        } else if (wkt.startsWith("GEOMETRYCOLLECTION")) {
            output = new GeoGeometryCollection();
            output.setCoordinatesFromWKT(wkt.substring(18).trim(), crs);
        }

        if (output != null)
            output.setCRS(crs);

        return output;
    }

    public static GeoGeometry fromDataStream(DataInputStream stream) throws IOException {
        int geometryType = stream.readInt();
        GeoGeometry result = null;
        switch (geometryType){
            case GeoConstants.GTPoint:
                result = new GeoPoint();
                result.read(stream);
                break;
            case GeoConstants.GTLineString:
                result = new GeoLineString();
                result.read(stream);
                break;
            case GeoConstants.GTLinearRing:
                result = new GeoLinearRing();
                result.read(stream);
                break;
            case GeoConstants.GTPolygon:
                result = new GeoPolygon();
                result.read(stream);
                break;
            case GeoConstants.GTMultiPoint:
                result = new GeoMultiPoint();
                result.read(stream);
                break;
            case GeoConstants.GTMultiLineString:
                result = new GeoMultiLineString();
                result.read(stream);
                break;
            case GeoConstants.GTMultiPolygon:
                result = new GeoMultiPolygon();
                result.read(stream);
                break;
            case GeoConstants.GTGeometryCollection:
                result = new GeoGeometryCollection();
                result.read(stream);
                break;
        }
        return result;
    }
} public class GeoMultiPoint
        extends GeoGeometryCollection
{
    protected static final long serialVersionUID = -1241179697270831765L;

    public GeoMultiPoint(GeoMultiPoint geoMultiPoint) {
        super(geoMultiPoint);
    }

    public GeoMultiPoint() {
        super();
    }


    @Override
    public void add(GeoGeometry geometry)
            throws ClassCastException
    {
        if (!(geometry instanceof GeoPoint)) {
            throw new ClassCastException("GeoMultiPoint: geometry is not GeoPoint type.");
        }

        super.add(geometry);
    }


    @Override
    public GeoPoint get(int index)
    {
        return (GeoPoint) mGeometries.get(index);
    }


    @Override
    public int getType()
    {
        return GeoConstants.GTMultiPoint;
    }


    @Override
    public void setCoordinatesFromJSON(JSONArray coordinates)
            throws JSONException
    {
        for (int i = 0; i < coordinates.length(); ++i) {
            GeoPoint point = new GeoPoint();
            point.setCoordinatesFromJSON(coordinates.getJSONArray(i));
            add(point);
        }
    }

    @Override
    public void setCoordinatesFromJSONStream(JsonReader reader, int crs) throws IOException {
        setCRS(crs);
        reader.beginArray();
        while (reader.hasNext()){
            GeoPoint pt = new GeoPoint();
            pt.setCoordinatesFromJSONStream(reader, crs);
            mGeometries.add(pt);
        }
        reader.endArray();
    }

    @Override
    public void setCoordinatesFromWKT(String wkt, int crs)
    {
        setCRS(crs);
        if (wkt.contains("EMPTY")) {
            return;
        }

        if (wkt.startsWith("(")) {
            wkt = wkt.substring(1, wkt.length() - 1);
        }
        for (String wktPt : wkt.split(",")) {
            GeoPoint pt = new GeoPoint();
            pt.setCoordinatesFromWKT(wktPt.trim(), crs);
            add(pt);
        }
    }

    @Override
    public GeoGeometry copy()
    {
        return new GeoMultiPoint(this);
    }

    public void add(GeoPoint point)
    {
        super.add(point);
    }


    @Override
    public String toWKT(boolean full)
    {
        StringBuilder buf = new StringBuilder();
        if (full) {
            buf.append("MULTIPOINT ");
        }
        if (mGeometries.size() == 0) {
            buf.append(" EMPTY");
        } else {
            buf.append("(");
            for (int i = 0; i < mGeometries.size(); i++) {
                if (i > 0) {
                    buf.append(", ");
                }
                GeoGeometry geom = mGeometries.get(i);
                buf.append(geom.toWKT(false));
            }
            buf.append(")");
        }
        return buf.toString();
    }

    @Override
    protected GeoGeometryCollection getInstance() {
        return new GeoMultiPoint();
    }

    @Override
    public GeoGeometry simplify(double tolerance) {
        GeoMultiPoint result = (GeoMultiPoint) copy();
        double checkTolerance = tolerance + tolerance;
        for(int i = 0; i < result.mGeometries.size(); i++){

            for(int j = 0; j < result.mGeometries.size(); j++){
                if(i == j)
                    continue;
                GeoPoint pt1 = (GeoPoint) result.mGeometries.get(i);
                GeoPoint pt2 = (GeoPoint) result.mGeometries.get(j);

                if(Math.abs(pt1.distance(pt2)) < checkTolerance) {
                    result.mGeometries.remove(j);
                    j--;
                }
            }
        }
        return result;
    }
} public class GeoMultiPolygon
        extends GeoGeometryCollection
{
    protected static final long serialVersionUID = -1241179697270831767L;


    public GeoMultiPolygon() {
        super();
    }


    public GeoMultiPolygon(GeoMultiPolygon multiPolygon) {
        this();

        for (int i = 0; i < multiPolygon.size(); i++)
            add(multiPolygon.get(i).copy());
    }


    @Override
    public void add(GeoGeometry geometry)
            throws ClassCastException
    {
        if (!(geometry instanceof GeoPolygon)) {
            throw new ClassCastException("GeoMultiPolygon: geometry is not GeoPolygon type.");
        }

        super.add(geometry);
    }


    @Override
    public GeoPolygon get(int index)
    {
        return (GeoPolygon) mGeometries.get(index);
    }


    @Override
    public int getType()
    {
        return GeoConstants.GTMultiPolygon;
    }


    @Override
    public void setCoordinatesFromJSON(JSONArray coordinates)
            throws JSONException
    {
        for (int i = 0; i < coordinates.length(); ++i) {
            GeoPolygon polygon = new GeoPolygon();
            polygon.setCoordinatesFromJSON(coordinates.getJSONArray(i));
            add(polygon);
        }
    }

    @Override
    public void setCoordinatesFromJSONStream(JsonReader reader, int crs) throws IOException {
        setCRS(crs);
        reader.beginArray();
        while (reader.hasNext()){
            GeoPolygon polygon = new GeoPolygon();
            polygon.setCoordinatesFromJSONStream(reader, crs);
            mGeometries.add(polygon);
        }
        reader.endArray();
    }

    @Override
    public String toWKT(boolean full)
    {
        StringBuilder buf = new StringBuilder();
        if (full) {
            buf.append("MULTIPOLYGON ");
        }
        if (mGeometries.size() == 0) {
            buf.append(" EMPTY");
        } else {
            buf.append("(");
            for (int i = 0; i < mGeometries.size(); i++) {
                if (i > 0) {
                    buf.append(", ");
                }
                GeoGeometry geom = mGeometries.get(i);
                buf.append(geom.toWKT(false));
            }
            buf.append(")");
        }
        return buf.toString();
    }


    @Override
    public void setCoordinatesFromWKT(String wkt, int crs)
    {
        setCRS(crs);
        if (wkt.contains("EMPTY")) {
            return;
        }

        if (wkt.startsWith("(")) {
            wkt = wkt.substring(1, wkt.length() - 1);
        }

        int pos = wkt.indexOf("((");
        while (pos != Constants.NOT_FOUND) {
            wkt = wkt.substring(pos + 1, wkt.length());
            pos = wkt.indexOf("))") - 1;
            if (pos < 1) {
                return;
            }

            GeoPolygon polygon = new GeoPolygon();
            polygon.setCoordinatesFromWKT(wkt.substring(0, pos).trim(), crs);
            add(polygon);

            pos = wkt.indexOf("((");
        }
    }


    public void add(GeoPolygon polygon)
    {
        super.add(polygon);
    }

    @Override
    public GeoGeometry copy()
    {
        return new GeoMultiPolygon(this);
    }

    @Override
    protected GeoGeometryCollection getInstance() {
        return new GeoMultiPolygon();
    }

    public void closeRings() {
        for (GeoGeometry polygon : mGeometries)
            if (polygon instanceof GeoPolygon)
                ((GeoPolygon) polygon).closeRings();
    }

    public boolean isSelfIntersects() {
        for (GeoGeometry polygon : mGeometries)
            if (polygon instanceof GeoPolygon)
                if (((GeoPolygon) polygon).intersects())
                    return true;

        return false;
    }

    public boolean isHolesInside() {
        for (GeoGeometry polygon : mGeometries)
            if (polygon instanceof GeoPolygon)
                if (!((GeoPolygon) polygon).isHolesInside())
                    return false;

        return true;
    }

    public boolean isHolesIntersect() {
        for (GeoGeometry polygon : mGeometries)
            if (polygon instanceof GeoPolygon)
                if (((GeoPolygon) polygon).isHolesIntersect())
                    return true;

        return false;
    }

    public double getPerimeter() {
        double length = 0;

        for (GeoGeometry polygon : mGeometries)
            if (polygon instanceof GeoPolygon)
                length += ((GeoPolygon) polygon).getPerimeter();

        return length;
    }

    public double getArea() {
        double area = 0;

        for (GeoGeometry polygon : mGeometries)
            if (polygon instanceof GeoPolygon)
                area += ((GeoPolygon) polygon).getArea();

        return area;
    }
} public class GeoPoint
        extends GeoGeometry
{
    protected static final long serialVersionUID = -1241179697270831762L;
    protected double mX;
    protected double mY;


    public GeoPoint()
    {
        mX = mY = 0.0;
    }


    public GeoPoint(
            double x,
            double y)
    {
        this.mX = x;
        this.mY = y;
    }


    public GeoPoint(final GeoPoint point)
    {
        this.mX = point.mX;
        this.mY = point.mY;
        this.mCRS = point.mCRS;
    }


    public final double getX()
    {
        return mX;
    }


    public void setX(double x)
    {
        mX = x;
    }


    public final double getY()
    {
        return mY;
    }


    public void setY(double y)
    {
        mY = y;
    }


    public void setCoordinates(
            double x,
            double y)
    {
        mX = x;
        mY = y;
    }


    public boolean equals(GeoPoint point)
    {
        return mX == point.mX && mY == point.mY;
    }

    public int compareTo(GeoPoint p) {
        if (this.mX == p.mX) {
            return Double.compare(this.mY, p.mY);
        } else {
            return Double.compare(this.mX, p.mX);
        }
    }

    @Override
    protected boolean rawProject(int toCrs)
    {
        switch (toCrs) {
            case CRS_WEB_MERCATOR:
                Geo.wgs84ToMercatorSphere(this);
                return super.rawProject(toCrs);
            case CRS_WGS84:
                Geo.mercatorToWgs84Sphere(this);
                return super.rawProject(toCrs);
            default:
                return false;
        }
    }


    @Override
    public GeoEnvelope getEnvelope()
    {
        return new GeoEnvelope(mX, mX, mY, mY);
    }


    @Override
    public JSONArray coordinatesToJSON()
            throws JSONException
    {
        JSONArray coordinates = new JSONArray();
        coordinates.put(mX);
        coordinates.put(mY);

        return coordinates;
    }


    @Override
    public final int getType()
    {
        return GeoConstants.GTPoint;
    }


    @Override
    public void setCoordinatesFromJSON(JSONArray coordinates)
            throws JSONException
    {
        mX = coordinates.getDouble(0);
        mY = coordinates.getDouble(1);
    }

    @Override
    public void setCoordinatesFromJSONStream(JsonReader reader, int crs) throws IOException {
        setCRS(crs);
        reader.beginArray();
        int pos = 0;
        while (reader.hasNext()) {
            if(pos == 0)
                mX = reader.nextDouble();
            else if(pos == 1)
                mY = reader.nextDouble();
            else
                reader.skipValue();
            pos++;
        }
        reader.endArray();
    }


    @Override
    public void setCoordinatesFromWKT(String wkt, int crs)
    {
        setCRS(crs);
        if (wkt.contains("EMPTY")) {
            return;
        }

        if (wkt.startsWith("(")) {
            wkt = wkt.substring(1, wkt.length() - 1);
        }
        int pos = wkt.indexOf(" ");
        if (pos == -1) {
            return;
        }
        mX = Double.parseDouble(wkt.substring(0, pos).trim());
        mY = Double.parseDouble(wkt.substring(pos).trim());
    }


    public String toString()
    {
        return "X: " + mX + ", Y: " + mY;
    }


    @Override
    public String toWKT(boolean full)
    {
        if (full) {
            return String.format(Locale.US, "POINT (%.8f %.8f)", mX, mY);//"POINT ( " + mX + " " + mY + " )";
        } else {
            return String.format(Locale.US, "%.8f %.8f", mX, mY);//mX + " " + mY;
        }

    }


    @Override
    public boolean equals(Object o)
    {
        if (!super.equals(o)) {
            return false;
        }
        GeoPoint otherPt = (GeoPoint) o;
        return getX() == otherPt.getX() && getY() == otherPt.getY();
    }


    @Override
    public boolean intersects(GeoEnvelope envelope)
    {
        return envelope.contains(this);
    }


    @Override
    public GeoGeometry copy()
    {
        return new GeoPoint(this);
    }


    @Override
    public void clear()
    {
        mX = mY = 0.0;
    }

    @Override
    public GeoGeometry simplify(double tolerance) {
        return new GeoPoint(this);
    }

    @Override
    public GeoGeometry clip(GeoEnvelope envelope) {
        return new GeoPoint(this);
    }

    @Override
    public void write(DataOutputStream stream) throws IOException {
        super.write(stream);
        stream.writeDouble(mX);
        stream.writeDouble(mY);
    }

    @Override
    public void read(DataInputStream stream) throws IOException{
        super.read(stream);
        mX = stream.readDouble();
        mY = stream.readDouble();
    }

    @Override
    public boolean isValid() {
        return inBounds();
    }

    public boolean inBounds() {
        if (getEnvelope().isInit()) {
            switch (mCRS) {
                case CRS_WGS84:
                    return mX >= -WGS_LONG_MAX && mX <= WGS_LONG_MAX && mY >= -WGS_LAT_MAX && mY < WGS_LAT_MAX;
                case CRS_WEB_MERCATOR:
                    return mX >= -MERCATOR_MAX && mX <= MERCATOR_MAX && mY >= -MERCATOR_MAX && mY < MERCATOR_MAX;
            }
        }

        return false;
    }

    @Override
    public double distance(GeoGeometry geometry) {
        if(geometry.getType() == GTPoint){
            GeoPoint pt = (GeoPoint) geometry;
            return Math.sqrt((pt.getX() - mX)*(pt.getX() - mX) + (pt.getY() - mY)*(pt.getY() - mY));
        }
        // TODO: 04.09.15 release for other types of geometries
        return 0;
    }
} public class GeoPolygon
        extends GeoGeometry
{
    protected static final long serialVersionUID = -1241179697270831764L;
    protected GeoLinearRing       mOuterRing;
    protected List<GeoLinearRing> mInnerRings;


    public GeoPolygon()
    {
        mOuterRing = new GeoLinearRing();
        mInnerRings = new LinkedList<>();
    }


    public GeoPolygon(GeoPolygon polygon)
    {
        mOuterRing = (GeoLinearRing) polygon.mOuterRing.copy();
        mInnerRings = new ArrayList<>();
        for (GeoLinearRing ring : polygon.mInnerRings) {
            mInnerRings.add((GeoLinearRing) ring.copy());
        }
    }


    public void add(GeoPoint point)
    {
        mOuterRing.add(point);
    }


    public GeoPoint remove(int index)
    {
        return mOuterRing.remove(index);
    }


    @Override
    protected boolean rawProject(int toCrs)
    {
        if (mOuterRing.rawProject(toCrs)) {
            boolean isOk = true;
            for (GeoGeometry geometry : mInnerRings) {
                isOk = isOk && geometry.rawProject(toCrs);
            }
            if (isOk) {
                super.rawProject(toCrs);
            }
            return isOk;
        }
        return false;
    }


    @Override
    public GeoEnvelope getEnvelope()
    {
        return mOuterRing.getEnvelope();
    }


    @Override
    public JSONArray coordinatesToJSON()
            throws JSONException
    {
        JSONArray coordinates = new JSONArray();
        coordinates.put(mOuterRing.coordinatesToJSON());

        for (GeoLinearRing innerRing : mInnerRings) {
            coordinates.put(innerRing.coordinatesToJSON());
        }

        return coordinates;
    }


    @Override
    public int getType()
    {
        return GeoConstants.GTPolygon;
    }


    @Override
    public void setCoordinatesFromJSON(JSONArray coordinates)
            throws JSONException
    {
        JSONArray ringCoordinates = coordinates.getJSONArray(0);

        if (ringCoordinates.length() < 4) {
            throw new JSONException(
                    "For type \"Polygon\", the \"coordinates\" member must be an array of LinearRing coordinate arrays. A LinearRing must be with 4 or more positions.");
        }

        mOuterRing.setCoordinatesFromJSON(ringCoordinates);

        if (!getOuterRing().isClosed()) {
            throw new JSONException(
                    "For type \"Polygon\", the \"coordinates\" member must be an array of LinearRing coordinate arrays. The first and last positions of LinearRing must be equivalent (they represent equivalent points).");
        }

        GeoLinearRing innerRing;

        for (int i = 1; i < coordinates.length(); i++) {
            ringCoordinates = coordinates.getJSONArray(i);

            if (ringCoordinates.length() < 4) {
                throw new JSONException(
                        "For type \"Polygon\", the \"coordinates\" member must be an array of LinearRing coordinate arrays. A LinearRing must be with 4 or more positions.");
            }

            innerRing = new GeoLinearRing();
            innerRing.setCoordinatesFromJSON(ringCoordinates);

            if (!innerRing.isClosed()) {
                throw new JSONException(
                        "For type \"Polygon\", the \"coordinates\" member must be an array of LinearRing coordinate arrays. The first and last positions of LinearRing must be equivalent (they represent equivalent points).");
            }

            addInnerRing(innerRing);
        }
    }
    @Override
    public void setCoordinatesFromJSONStream(JsonReader reader, int crs) throws IOException {
        setCRS(crs);
        boolean outerRingFilled = false;
        reader.beginArray();
        while (reader.hasNext()){
            if (!outerRingFilled) {
                mOuterRing.setCoordinatesFromJSONStream(reader, crs);
                outerRingFilled = true;
            } else {
                GeoLinearRing ring = new GeoLinearRing();
                ring.setCoordinatesFromJSONStream(reader, crs);
                mInnerRings.add(ring);
            }
        }
        reader.endArray();
    }


    @Override
    public void setCoordinatesFromWKT(String wkt, int crs)
    {
        setCRS(crs);
        if (wkt.contains("EMPTY")) {
            return;
        }

        if (wkt.startsWith("(")) {
            wkt = wkt.substring(1, wkt.length() - 1);
        }
        //get outer ring
        int pos = wkt.indexOf(")");
        if (pos == Constants.NOT_FOUND) // no inner rings
        {
            mOuterRing.setCoordinatesFromWKT(wkt, crs);
        } else {
            mOuterRing.setCoordinatesFromWKT(wkt.substring(0, pos), crs);
        }
        pos = wkt.indexOf("(");
        while (pos != Constants.NOT_FOUND) {
            wkt = wkt.substring(pos + 1, wkt.length());
            pos = wkt.indexOf(")") - 1;
            if (pos < 1) {
                return;
            }

            GeoLinearRing innerRing = new GeoLinearRing();
            innerRing.setCoordinatesFromWKT(wkt.substring(0, pos), crs);
            mInnerRings.add(innerRing);

            pos = wkt.indexOf("(");
        }
    }


    public GeoLinearRing getOuterRing()
    {
        return mOuterRing;
    }


    @Override
    public String toWKT(boolean full)
    {
        StringBuilder buf = new StringBuilder();
        if (full) {
            buf.append("POLYGON ");
        }
        if (mOuterRing.getPoints().size() == 0) {
            buf.append(" EMPTY");
        } else {
            buf.append("(");
            buf.append(mOuterRing.toWKT(false));
            if (mInnerRings.size() > 0) {
                buf.append(", ");
                for (int i = 0; i < mInnerRings.size(); i++) {
                    GeoLinearRing ring = mInnerRings.get(i);
                    buf.append(ring.toWKT(false));
                }
            }
            buf.append(")");
        }
        return buf.toString();
    }


    @Override
    public boolean equals(Object o)
    {
        if (!super.equals(o)) {
            return false;
        }
        GeoPolygon otherPlg = (GeoPolygon) o;
        if (!otherPlg.getOuterRing().equals(getOuterRing())) {
            return false;
        }
        for (int i = 0; i < mInnerRings.size(); i++) {
            GeoLinearRing ring = mInnerRings.get(i);
            GeoLinearRing otherRing = otherPlg.getInnerRing(i);
            if (!ring.equals(otherRing)) {
                return false;
            }
        }
        return true;
    }


    public GeoLinearRing getInnerRing(int index)
    {
        if (mInnerRings.size() > index) {
            return mInnerRings.get(index);
        }
        return null;
    }


    public int getInnerRingCount()
    {
        return mInnerRings.size();
    }


    public void removeInnerRing(int index)
    {
        mInnerRings.remove(index);
    }


    public void addInnerRing(GeoLinearRing ring)
    {
        mInnerRings.add(ring);
    }


    @Override
    public boolean intersects(GeoEnvelope envelope)
    {
        if (super.intersects(envelope)) {
            //check if inside outer ring but not in hole

            boolean intersects = mOuterRing.intersects(envelope);
            if (!intersects) {
                return false;
            }

            for (GeoLinearRing ring : mInnerRings) {
                if (ring.contains(envelope)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }


    @Override
    public GeoGeometry copy()
    {
        return new GeoPolygon(this);
    }


    @Override
    public void clear()
    {
        mOuterRing.clear();
        mInnerRings.clear();
    }

    @Override
    public GeoGeometry simplify(double tolerance) {
        GeoGeometry outerRingSimple = mOuterRing.simplify(tolerance);
        if(outerRingSimple == null) // this is point
            return null;

        GeoPolygon result = new GeoPolygon();
        result.setCRS(mCRS);
        result.mOuterRing = (GeoLinearRing) outerRingSimple;
        for(GeoLinearRing ring : mInnerRings){
            GeoLinearRing innerRingSimple = ring.simplify(tolerance, true);
            if(innerRingSimple == null) // skip points
                continue;
            result.addInnerRing(innerRingSimple);
        }

        return result;
    }

    @Override
    public GeoGeometry clip(GeoEnvelope envelope) {
        GeoPolygon result = new GeoPolygon();
        result.setCRS(mCRS);
        result.mOuterRing = (GeoLinearRing) mOuterRing.clip(envelope);

        if(result.mOuterRing.getPointCount() < 4)
            return null;

        for(GeoLinearRing ring : mInnerRings){
            GeoLinearRing geometry = (GeoLinearRing) ring.clip(envelope);
            if(null != geometry)
                result.addInnerRing(geometry);
        }

        return result;
    }

    @Override
    public void read(DataInputStream stream) throws IOException {
        super.read(stream);
        mOuterRing = (GeoLinearRing) GeoGeometryFactory.fromDataStream(stream);
        int innerRingCount = stream.readInt();
        for (int i = 0; i < innerRingCount; i++){
            GeoGeometry geometry = GeoGeometryFactory.fromDataStream(stream);
            if(null != geometry && geometry instanceof GeoLinearRing){
                mInnerRings.add((GeoLinearRing) geometry);
            }
        }
    }

    @Override
    public boolean isValid() {
        for (GeoLinearRing ring : mInnerRings)
            if (!ring.isValid())
                return false;

        return mOuterRing.isValid();
    }

    @Override
    public double distance(GeoGeometry geometry) {
        return mOuterRing.distance(geometry);
    }

    @Override
    public void write(DataOutputStream stream) throws IOException {
        super.write(stream);
        mOuterRing.write(stream);
        int innerRingCount = mInnerRings.size();
        stream.writeInt(innerRingCount);
        for (int i = 0; i < innerRingCount; i++){
            GeoLinearRing ring = mInnerRings.get(i);
            ring.write(stream);
        }
    }

    public void closeRings() {
        mOuterRing.closeRing();

        for (int i = 0; i < getInnerRingCount(); i++)
            getInnerRing(i).closeRing();
    }

    public boolean intersects() {
        if (mOuterRing.intersects())
            return true;

        for (GeoLinearRing ring : mInnerRings)
            if (ring.intersects())
                return true;

        return false;
    }

    public boolean isHolesInside() {
        mOuterRing.closeRing();

        for (GeoLinearRing ring: mInnerRings) {
            ring.closeRing();

            if (!mOuterRing.getEnvelope().contains(ring.getEnvelope()))
                return false;

            for (int i = 0; i < ring.getPointCount(); i++)
                if (!contains(ring.getPoint(i)))
                    return false;
        }

        return true;
    }

    public boolean isHolesIntersect() {
        for (int i = 0; i < mInnerRings.size() - 1; i++)
            for (int j = i + 1; j < mInnerRings.size(); j++)
                if (mInnerRings.get(i).intersects(mInnerRings.get(j)))
                    return true;

        return false;
    }

    // https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
    // http://stackoverflow.com/a/2922778
    public boolean contains(GeoPoint point) {
        int i, j;
        double vertiy, vertix, vertjy, vertjx;
        boolean c = false;

        for (i = 0, j = mOuterRing.getPointCount() - 1; i < mOuterRing.getPointCount(); j = i++) {
            vertix = mOuterRing.getPoints().get(i).getX();
            vertiy = mOuterRing.getPoints().get(i).getY();
            vertjx = mOuterRing.getPoints().get(j).getX();
            vertjy = mOuterRing.getPoints().get(j).getY();

            if (((vertiy > point.getY()) != (vertjy > point.getY())) &&
                    (point.getX() < (vertjx - vertix) * (point.getY() - vertiy) / (vertjy - vertiy) + vertix))
                c = !c;
        }

        return c;
    }

    public double getPerimeter() {
        if (mOuterRing != null)
            return mOuterRing.getLength();
        else
            return 0;
    }

    public double getArea() {
        return getArea(this);
    }

    private static double getArea(GeoPolygon polygon) {
        double area = getArea(polygon.getOuterRing());

        for (GeoLinearRing ring : polygon.mInnerRings)
            area -= getArea(ring);

        return area;
    }

    // based on https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/SphericalUtil.java
    /**
     * Returns the signed area of a closed path on a sphere of given radius.
     * The computed area uses the same units as the radius squared.
     * Used by SphericalUtilTest.
     */
    private static double getArea(GeoLinearRing ring) {
        if (ring == null)
            return 0;

        List<GeoPoint> coordinates = ring.getPoints();
        int size = coordinates.size();
        if (size < 3)
            return 0;

        double total = 0;
        GeoPoint p = (GeoPoint) coordinates.get(size - 1).copy();
        p.setCRS(CRS_WEB_MERCATOR);
        p.project(CRS_WGS84);

        double prevTanLat = Math.tan((Math.PI / 2 - Math.toRadians(p.getY())) / 2);
        double prevLng = Math.toRadians(p.getX());
        // For each edge, accumulate the signed area of the triangle formed by the North Pole
        // and that edge ("polar triangle").
        for (GeoPoint point : coordinates) {
            p = (GeoPoint) point.copy();
            p.setCRS(CRS_WEB_MERCATOR);
            p.project(CRS_WGS84);

            double tanLat = Math.tan((Math.PI / 2 - Math.toRadians(p.getY())) / 2);
            double lng = Math.toRadians(p.getX());
            total += polarTriangleArea(tanLat, lng, prevTanLat, prevLng);
            prevTanLat = tanLat;
            prevLng = lng;
        }

        return Math.abs(total) * (6378137f * 6378137f);
    }

    /**
     * Returns the signed area of a triangle which has North Pole as a vertex.
     * Formula derived from "Area of a spherical triangle given two edges and the included angle"
     * as per "Spherical Trigonometry" by Todhunter, page 71, section 103, point 2.
     * See http://books.google.com/books?id=3uBHAAAAIAAJ&pg=PA71
     * The arguments named "tan" are tan((pi/2 - latitude)/2).
     */
    private static double polarTriangleArea(double tan1, double lng1, double tan2, double lng2) {
        double deltaLng = lng1 - lng2;
        double t = tan1 * tan2;
        return 2 * Math.atan2(t * Math.sin(deltaLng), 1 + t * Math.cos(deltaLng));
    }

    public void setInnerRing(int location, GeoLinearRing ring) {
        mInnerRings.set(location, ring);
    }

    public void setOuterRing(GeoLinearRing ring) {
        mOuterRing = ring;
    }
} public class GeometryRTree implements IGeometryCache {

    public enum SeedPicker { LINEAR, QUADRATIC }
    private final SeedPicker seedPicker;

    private int maxEntries;
    private int minEntries;
    private Node root;

    private volatile int size;

    protected File mPath;
    protected boolean mHasEdits;

    /**
     * Creates a new RTree.
     *
     * @param maxEntries
     *          maximum number of entries per node
     * @param minEntries
     *          minimum number of entries per node (except for the root node)
     */
    public GeometryRTree(int maxEntries, int minEntries, SeedPicker seedPicker){
        if (minEntries > (maxEntries / 2)) throw new AssertionError();
        this.maxEntries = maxEntries;
        this.minEntries = minEntries;
        this.seedPicker = seedPicker;
        root = buildRoot(true);
        mHasEdits = false;
    }

    public GeometryRTree(int maxEntries, int minEntries){
        this(maxEntries, minEntries, SeedPicker.LINEAR);
    }

    private Node buildRoot(boolean asLeaf){
        return new Node(new GeoEnvelope(-GeoConstants.MERCATOR_MAX, GeoConstants.MERCATOR_MAX,
                                        -GeoConstants.MERCATOR_MAX, GeoConstants.MERCATOR_MAX),
                        asLeaf);
    }

    /**
     * Builds a new RTree using default parameters: maximum 50 entries per node
     * minimum 2 entries per node
     */
    public GeometryRTree(){
        this(8, 2, SeedPicker.QUADRATIC);
    }

    /**
     * @return the maximum number of entries per node
     */
    public int getMaxEntries()
    {
        return maxEntries;
    }

    /**
     * @return the minimum number of entries per node for all nodes except the
     *         root.
     */
    public int getMinEntries()
    {
        return minEntries;
    }

    @Override
    public boolean isItemExist(long featureId) {
        return isItemExist(featureId, root);
    }

    public boolean isItemExist(long featureId, Node n) {
        if (n.mLeaf)
        {
            for (Node e : n.mChildren){
                if (!e.isNode()) {
                    Entry entry = (Entry) e;
                    if (entry.getFeatureId() == featureId) {
                        return true;
                    }
                }
            }
        }
        else{
            for (Node c : n.mChildren)
            {
                if (isItemExist(featureId, c))
                {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public IGeometryCacheItem addItem(long id, GeoEnvelope envelope) {
        mHasEdits = true;
        return insert(id, envelope);
    }

    @Override
    public IGeometryCacheItem getItem(long featureId) {
        return getItem(featureId, root);
    }

    @Override
    public void changeId(long oldFeatureId, long newFeatureId) {
        IGeometryCacheItem item = getItem(oldFeatureId);
        if(null != item)
            item.setFeatureId(newFeatureId);
    }

    @Override
    public synchronized void save(File path) {

        boolean isSameFile = null != mPath && mPath.equals(path);

        if(isSameFile && !mHasEdits)
            return;

        try {
            FileUtil.createDir(path.getParentFile());
            FileOutputStream fileOutputStream = new FileOutputStream(path);
            DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);

            dataOutputStream.writeInt(maxEntries);
            dataOutputStream.writeInt(minEntries);
            dataOutputStream.writeInt(size);

            root.write(dataOutputStream);

            dataOutputStream.flush();
            dataOutputStream.close();
            fileOutputStream.close();

            mHasEdits = false;
        } catch (RuntimeException | IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void load(File path) {
        clear();

        if (!path.exists()) {
            return;
        }

        mPath = path;

        try {
            FileInputStream fileInputStream = new FileInputStream(path);
            DataInputStream dataInputStream = new DataInputStream(fileInputStream);

            maxEntries = dataInputStream.readInt();
            minEntries = dataInputStream.readInt();
            size = dataInputStream.readInt();

            dataInputStream.readBoolean();
            root = new Node();
            root.read(dataInputStream);

            dataInputStream.close();
            fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public IGeometryCacheItem getItem(long featureId, Node n) {
        if (n.mLeaf){
            for (Node e : n.mChildren){
                if (!e.isNode()) {
                    Entry entry = (Entry) e;
                    if (entry.getFeatureId() == featureId){
                        return entry;
                    }
                }
            }
        }
        else{
            for (Node c : n.mChildren)
            {
                IGeometryCacheItem entry = getItem(featureId, c);
                if (null != entry){
                    return entry;
                }
            }
        }
        return null;
    }

    /**
     * @return the number of items in this tree.
     */
    public int size(){
        return size;
    }

    /**
     * Searches the RTree for objects overlapping with the given rectangle.
     *
     * @param extent
     *          the envelope to search
     * @return a list of objects whose envelopes overlap with the given
     *         envelope.
     */
    @Override
    public synchronized List<IGeometryCacheItem> search(GeoEnvelope extent){
        LinkedList<IGeometryCacheItem> results = new LinkedList<>();
        search(extent, root, results);
        return results;
    }

    @Override
    public synchronized List<IGeometryCacheItem> getAll() {
        LinkedList<IGeometryCacheItem> result = new LinkedList<>();
        getAll(root, result);
        return result;
    }

    protected void getAll(Node n, LinkedList<IGeometryCacheItem> results){
        if (n.mLeaf){
            for (Node e : n.mChildren){
                if (!e.isNode()) {
                    Entry entry = (Entry) e;
                    results.add(entry);
                }
            }
        }
        else{
            for (Node c : n.mChildren){
                getAll(c, results);
            }
        }
    }

    private void search(GeoEnvelope extent, Node n,
                        LinkedList<IGeometryCacheItem> results){
        if (n.mLeaf)
        {
            for (Node e : n.mChildren)
            {
                if (e.mCoords.intersects(extent) && e instanceof Entry) {
                    Entry entry = (Entry)e;
                    results.add(entry);
                }
            }
        }
        else
        {
            for (Node c : n.mChildren)
            {
                if (c.mCoords.intersects(extent))
                {
                    search(extent, c, results);
                }
            }
        }
    }

    /**
     * Deletes the entry associated with the given rectangle from the RTree
     *
     * @param featureId
     *          the feature id to delete
     * @return true if the entry was deleted from the RTree.
     */
    @Override
    public IGeometryCacheItem removeItem(long featureId){
        Node l = findLeaf(root, featureId);
        if ( l == null ) {
            return null;
        }

        mHasEdits = true;

        condenseTree(l);
            size--;

        if ( size == 0 ){
            root = buildRoot(true);
        }

        if(l instanceof IGeometryCache)
            return (IGeometryCacheItem) l;
        return null;
    }

    private Node findLeaf(Node n, long featureId){
        if (n.mLeaf){
            for (Node c : n.mChildren){
                if (!c.isNode() && ((Entry) c).mFeatureId == featureId)
                {
                    return c;
                }
            }
        }
        else{
            for (Node c : n.mChildren){
                Node result = findLeaf(c, featureId);
                if (result != null)
                {
                    return result;
                }
            }
        }
        return null;
    }

    private void condenseTree(Node n){
        Set<Node> q = new HashSet<>();
        while (n != root){
            if (n.mLeaf && (n.mChildren.size() < minEntries)){
                q.addAll(n.mChildren);
                n.mParent.mChildren.remove(n);
            }
            else if (!n.mLeaf && (n.mChildren.size() < minEntries)){
                // probably a more efficient way to do this...
                LinkedList<Node> toVisit = new LinkedList<>(n.mChildren);
                while (!toVisit.isEmpty()){
                    Node c = toVisit.removeFirst();
                    if (c.mLeaf){
                        q.addAll(c.mChildren);

                    }
                    else{
                        toVisit.addAll(c.mChildren);
                    }
                }
                n.mParent.mChildren.remove(n);
            }
            else{
                tighten(n);
            }
            n = n.mParent;
        }
        if ( root.mChildren.isEmpty() ){
            root = buildRoot(true);
        }
        else if ( (root.mChildren.size() == 1) && (!root.mLeaf) ){
            root = root.mChildren.get(0);
            root.mParent = null;
        }
        else{
            tighten(root);
        }

        for (Node ne : q){
            if (!ne.isNode()) {
                Entry e = (Entry) ne;
                insert(e.mFeatureId, e.mCoords);
            }
        }
        size -= q.size();
    }

    /**
     * Empties the RTree
     */
    public void clear(){
        root = buildRoot(true);
        mHasEdits = false;
        // let the GC take care of the rest.
    }

    /**
     * Inserts the given entry into the RTree, associated with the given
     * rectangle.
     *
     * @param featureId
     *          a feature identificator
     * @param envelope
     *          an envelope
     */
    public IGeometryCacheItem insert(long featureId, GeoEnvelope envelope){
        Entry e = new Entry(featureId, envelope);
        Node l = chooseLeaf(root, e);
        if(l == null)
            l = root;

        l.add(e);
        size++;
        if (l.mChildren.size() > maxEntries){
            Node[] splits = splitNode(l);
            adjustTree(splits[0], splits[1]);
        }
        else{
            adjustTree(l, null);
        }

        return e;
    }

    private void adjustTree(Node n, Node nn){
        if (n == root){
            if (nn != null){
                // build new root and add children.
                root = buildRoot(false);
                root.add(n);
                root.add(nn);
            }
            tighten(root);
            return;
        }
        tighten(n);
        if (nn != null){
            tighten(nn);
            if (n.mParent.mChildren.size() > maxEntries){
                Node[] splits = splitNode(n.mParent);
                adjustTree(splits[0], splits[1]);
            }
        }
        if (n.mParent != null){
            adjustTree(n.mParent, null);
        }
    }

    private Node[] splitNode(Node n){
        // TODO: this class probably calls "tighten" a little too often.
        // For instance the call at the end of the "while (!cc.isEmpty())" loop
        // could be modified and inlined because it's only adjusting for the addition
        // of a single node.  Left as-is for now for readability.
        @SuppressWarnings("unchecked")
        Node[] nn = new GeometryRTree.Node[]{ n, new Node(n.mCoords, n.mLeaf) };
        nn[1].mParent = n.mParent;
        if (nn[1].mParent != null){
            nn[1].mParent.add(nn[1]);
        }
        LinkedList<Node> cc = new LinkedList<>(n.mChildren);
        n.mChildren.clear();
        Node[] ss = seedPicker == SeedPicker.LINEAR ? lPickSeeds(cc) : qPickSeeds(cc);
        nn[0].add(ss[0]);
        nn[1].add(ss[1]);
        tighten(nn);
        while (!cc.isEmpty()){
            if ((nn[0].mChildren.size() >= minEntries) && (nn[1].mChildren.size() + cc.size() == minEntries)){
                nn[1].addAll(cc);
                cc.clear();
                tighten(nn); // Not sure this is required.
                return nn;
            }
            else if ((nn[1].mChildren.size() >= minEntries) && (nn[0].mChildren.size() + cc.size() == minEntries)){
                nn[0].addAll(cc);
                cc.clear();
                tighten(nn); // Not sure this is required.
                return nn;
            }

            Node c = seedPicker == SeedPicker.LINEAR ? lPickNext(cc) : qPickNext(cc, nn);
            if (c == null)
                return nn;

            Node preferred;
            double e0 = getRequiredExpansion(nn[0].mCoords, c);
            double e1 = getRequiredExpansion(nn[1].mCoords, c);
            if (e0 < e1){
                preferred = nn[0];
            }
            else if (e0 > e1){
                preferred = nn[1];
            }
            else{
                double a0 = nn[0].mCoords.getArea();
                double a1 = nn[1].mCoords.getArea();
                if (a0 < a1){
                    preferred = nn[0];
                }
                else if (e0 > a1){
                    preferred = nn[1];
                }
                else{
                    if (nn[0].mChildren.size() < nn[1].mChildren.size()){
                        preferred = nn[0];
                    }
                    else if (nn[0].mChildren.size() > nn[1].mChildren.size()){
                        preferred = nn[1];
                    }
                    else{
                        preferred = nn[(int) Math.round(Math.random())];
                    }
                }
            }
            preferred.add(c);
            tighten(preferred);
        }
        return nn;
    }

    // Implementation of Quadratic PickSeeds
    private Node[] qPickSeeds(LinkedList<Node> nn){
        @SuppressWarnings("unchecked")
        Node[] bestPair = new Node[2];
        double maxWaste = -1.0f * Double.MAX_VALUE;
        for (Node n1: nn){
            for (Node n2: nn){
                if (n1 == n2)
                    continue;
                double n1a = n1.mCoords.getArea();
                double n2a = n2.mCoords.getArea();
                double ja = 1.0f;
                double jc0 = Math.min(n1.mCoords.getMinX(), n2.mCoords.getMinX());
                double jc1 = Math.max(n1.mCoords.getMaxX(), n2.mCoords.getMaxX());
                ja *= (jc1 - jc0);
                jc0 = Math.min(n1.mCoords.getMinY(), n2.mCoords.getMinY());
                jc1 = Math.max(n1.mCoords.getMaxY(), n2.mCoords.getMaxY());
                ja *= (jc1 - jc0);

                double waste = ja - n1a - n2a;
                if ( waste > maxWaste )
                {
                    maxWaste = waste;
                    bestPair[0] = n1;
                    bestPair[1] = n2;
                }
            }
        }
        nn.remove(bestPair[0]);
        nn.remove(bestPair[1]);
        return bestPair;
    }

    /**
     * Implementation of QuadraticPickNext
     * @param cc the children to be divided between the new nodes, one item will be removed from this list.
     * @param nn the candidate nodes for the children to be added to.
     */
    private Node qPickNext(LinkedList<Node> cc, Node[] nn){
        double maxDiff = -1.0f * Double.MAX_VALUE;
        Node nextC = null;
        for ( Node c: cc )
        {
            double n0Exp = getRequiredExpansion(nn[0].mCoords, c);
            double n1Exp = getRequiredExpansion(nn[1].mCoords, c);
            double diff = Math.abs(n1Exp - n0Exp);
            if (diff > maxDiff){
                maxDiff = diff;
                nextC = c;
            }
        }
        assert (nextC != null) : "No node selected from qPickNext";
        cc.remove(nextC);
        return nextC;
    }

    // Implementation of LinearPickSeeds
    private Node[] lPickSeeds(LinkedList<Node> nn){
        @SuppressWarnings("unchecked")
        Node[] bestPair = new Node[2];
        boolean foundBestPair = false;
        double bestSep = 0.0f;

        double dimLb = Double.MAX_VALUE, dimMinUb = Double.MAX_VALUE;
        double dimUb = Double.MIN_VALUE, dimMaxLb = Double.MIN_VALUE;
        Node nMaxLb = null, nMinUb = null;
        for (Node n : nn)
        {
            if (n.mCoords.getMinX() < dimLb)
            {
                dimLb = n.mCoords.getMinX();
            }
            if (n.mCoords.getMaxX() > dimUb)
            {
                dimUb = n.mCoords.getMaxX();
            }
            if (n.mCoords.getMinX() > dimMaxLb)
            {
                dimMaxLb = n.mCoords.getMinX();
                nMaxLb = n;
            }
            if (n.mCoords.getMaxX() < dimMinUb)
            {
                dimMinUb = n.mCoords.getMaxX();
                nMinUb = n;
            }


        }
        double sep = (nMaxLb == nMinUb) ? -1.0f :
                Math.abs((dimMinUb - dimMaxLb) / (dimUb - dimLb));
        if (sep >= bestSep)
        {
            // FIXME
            // nMaxLb/nMinLb is null if dimMaxLb/dimMinUb is not set
            bestPair[0] = nMaxLb;
            bestPair[1] = nMinUb;
            bestSep = sep;
            foundBestPair = true;
        }

        dimLb = Double.MAX_VALUE;
        dimMinUb = Double.MAX_VALUE;
        dimUb = Double.MIN_VALUE;
        dimMaxLb = Double.MIN_VALUE;
        nMaxLb = null;
        nMinUb = null;

        for (Node n : nn)
        {
            if (n.mCoords.getMinY() < dimLb)
            {
                dimLb = n.mCoords.getMinY();
            }
            if (n.mCoords.getMaxY() > dimUb)
            {
                dimUb = n.mCoords.getMaxY();
            }
            if (n.mCoords.getMinY() > dimMaxLb)
            {
                dimMaxLb = n.mCoords.getMinY();
                nMaxLb = n;
            }
            if (n.mCoords.getMaxY() < dimMinUb)
            {
                dimMinUb = n.mCoords.getMaxY();
                nMinUb = n;
            }

        }
        sep = (nMaxLb == nMinUb) ? -1.0f :
                Math.abs((dimMinUb - dimMaxLb) / (dimUb - dimLb));
        if (sep >= bestSep)
        {
            bestPair[0] = nMaxLb;
            bestPair[1] = nMinUb;
            foundBestPair = true;
        }

        // In the degenerate case where all points are the same, the above
        // algorithm does not find a best pair.  Just pick the first 2
        // children.
        if ( !foundBestPair ){
            bestPair = new Node[] { nn.get(0), nn.get(1) };
        }
        nn.remove(bestPair[0]);
        nn.remove(bestPair[1]);
        return bestPair;
    }

    /**
     * Implementation of LinearPickNext
     * @param cc the children to be divided between the new nodes, one item will be removed from this list.
     */
    private Node lPickNext(LinkedList<Node> cc){
        return cc.removeFirst();
    }

    private void tighten(Node... nodes){
        if (nodes.length < 1) throw new AssertionError("Pass some nodes to tighten!");
        for (Node n: nodes) {
            if (n.mChildren.size() <= 0) throw new AssertionError("tighten() called on empty node!");
            n.mCoords.unInit();

            try {
                for (Node c : n.mChildren) {
                    // we may have bulk-added a bunch of children to a node (eg. in
                    // splitNode)
                    // so here we just enforce the child->parent relationship.
                    //c.mParent = n;

                    n.mCoords.merge(c.mCoords);
                }
            } catch (ConcurrentModificationException ignored) {}
        }
    }

    private Node chooseLeaf(Node n, Entry e)
    {
        if (n == null)
            return null;

        if (n.mLeaf)
            return n;

        double minInc = Double.MAX_VALUE;
        Node next = null;
        for (Node c : n.mChildren) {
            double inc = getRequiredExpansion(c.mCoords, e);
            if (inc < minInc) {
                minInc = inc;
                next = c;
            } else if (inc == minInc) {
                if (next == null)
                    continue;

                double curArea = next.mCoords.getArea();
                double thisArea = c.mCoords.getArea();
                if (thisArea < curArea) {
                    next = c;
                }
            }
        }

        if (next == null)
            return n;

        return chooseLeaf(next, e);
    }

    /**
     * Returns the increase in area necessary for the given rectangle to cover the
     * given entry.
     */
    private double getRequiredExpansion(GeoEnvelope envelope, Node e)
    {
        double area = envelope.getArea();
        double deltaX = 0.0, deltaY = 0.0;
        if (envelope.mMaxX < e.mCoords.mMaxX){
            deltaX = e.mCoords.mMaxX - envelope.mMaxX;
        }
        else if (envelope.mMaxX > e.mCoords.mMaxX){
            deltaX = envelope.mMinX - e.mCoords.mMinX;
        }

        if (envelope.mMaxY < e.mCoords.mMaxY){
            deltaY = e.mCoords.mMaxY - envelope.mMaxY;
        }
        else if (envelope.mMaxY > e.mCoords.mMaxY){
            deltaY = envelope.mMinY - e.mCoords.mMinY;
        }

        double expanded = (envelope.width() + deltaX) * (envelope.height() + deltaY);
        return (expanded - area);
    }

    protected class Node
    {
        protected GeoEnvelope mCoords;
        protected LinkedList<Node> mChildren;
        protected boolean mLeaf;

        protected Node mParent;

        protected Node(){
            mCoords = new GeoEnvelope();
            mChildren = new LinkedList<>();
        }

        public int size(){
            return mChildren.size();
        }

        public void add(Node node){
            mChildren.add(node);
            node.setParent(this);
        }

        public void addAll(LinkedList<Node> children){
            for (Node node : children){
                add(node);
            }
        }

        protected Node(GeoEnvelope coords, boolean leaf)
        {
            mCoords = new GeoEnvelope(coords);
            mLeaf = leaf;
            mChildren = new LinkedList<>();
        }

        public void write(DataOutputStream stream) throws IOException {
            stream.writeBoolean(isNode());
            stream.writeBoolean(mLeaf);
            stream.writeDouble(mCoords.getMinX());
            stream.writeDouble(mCoords.getMinY());
            stream.writeDouble(mCoords.getMaxX());
            stream.writeDouble(mCoords.getMaxY());
            stream.writeInt(mChildren.size());
            for(Node node : mChildren){
                node.write(stream);
            }
        }

        public void read(DataInputStream stream) throws IOException {
            mLeaf = stream.readBoolean();
            double minX = stream.readDouble();
            double minY = stream.readDouble();
            double maxX = stream.readDouble();
            double maxY = stream.readDouble();
            mCoords.setMin(minX, minY);
            mCoords.setMax(maxX, maxY);
            int size = stream.readInt();
            for(int i = 0; i < size; i++){
                if(stream.readBoolean()){
                    Node childNode = new Node();
                    childNode.read(stream);
                    add(childNode);
                }
                else {
                    Entry childEntry = new Entry();
                    childEntry.read(stream);
                    add(childEntry);
                }
            }
        }

        protected boolean isNode(){ return true; }

        public void setParent(Node parent) {
            mParent = parent;
        }
    }

    protected class Entry extends Node implements IGeometryCacheItem
    {
        protected long mFeatureId;

        protected Entry()
        {
            super();
        }

        protected Entry(long featureId, GeoEnvelope envelope)
        {
            super(envelope, true);
            mFeatureId = featureId;
        }

        public boolean intersects(GeoEnvelope extent) {
            return mCoords.intersects(extent);
        }

        @Override
        public GeoEnvelope getEnvelope() {
            return mCoords;
        }

        @Override
        public long getFeatureId() {
            return mFeatureId;
        }

        @Override
        public void setFeatureId(long id) {
            mFeatureId = id;
        }

        @Override
        public void read(DataInputStream stream) throws IOException {
            super.read(stream);
            mFeatureId = stream.readLong();
        }

        @Override
        public void write(DataOutputStream stream) throws IOException {
            super.write(stream);
            stream.writeLong(mFeatureId);
        }

        @Override
        protected boolean isNode() {
            return false;
        }
    }
} public class Connection
        implements INGWResource
{
    protected int mNgwVersionMajor = Constants.NOT_FOUND;
    protected int mNgwVersionMinor = Constants.NOT_FOUND;

    protected String        mName;
    protected String        mLogin;
    protected String        mPassword;
    protected String        mURL;
    protected boolean       mIsConnected;
    protected String        mCookie;
    protected List<Integer> mSupportedTypes;
    protected ResourceGroup mRootResource;
    protected int           mId;
    protected INGWResource  mParent;

    public final static int NGWResourceTypeNone              = 1 << 0;
    public final static int NGWResourceTypeResourceGroup     = 1 << 1;
    public final static int NGWResourceTypePostgisLayer      = 1 << 2;
    public final static int NGWResourceTypePostgisConnection = 1 << 3;
    public final static int NGWResourceTypeWMSServerService  = 1 << 4;
    public final static int NGWResourceTypeBaseLayers        = 1 << 5;
    public final static int NGWResourceTypeWebMap            = 1 << 6;
    public final static int NGWResourceTypeWFSServerService  = 1 << 7;
    public final static int NGWResourceTypeVectorLayer       = 1 << 8;
    public final static int NGWResourceTypeRasterLayer       = 1 << 9;
    public final static int NGWResourceTypeVectorLayerStyle  = 1 << 10;
    public final static int NGWResourceTypeRasterLayerStyle  = 1 << 11;
    public final static int NGWResourceTypeFileSet           = 1 << 12;
    public final static int NGWResourceTypeConnection        = 1 << 13;
    public final static int NGWResourceTypeConnections       = 1 << 14;
    public final static int NGWResourceTypeWMSClient         = 1 << 15;
    public final static int NGWResourceTypeLookupTable       = 1 << 16;


    public Connection(
            String name,
            String login,
            String password,
            String url)
    {
        mName = name;
        mLogin = login;
        mPassword = password;
        mURL = NGWUtil.getServerUrl(url);
        mIsConnected = false;
        mId = Connections.getNewId();
        mSupportedTypes = new ArrayList<>();
    }

    public boolean connect(boolean guest) {
        setNgwVersion();
        fillCapabilities();

        mRootResource = new ResourceGroup(0, this);
        mRootResource.setParent(this);

        if (!guest) {
            try {
                AtomicReference<String> reference = new AtomicReference<>(mURL);
                mCookie = NGWUtil.getConnectionCookie(reference, mLogin, mPassword);
                if (null == mCookie) {
                    return false;
                }
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
        }

        mIsConnected = true;
        return true;
    }

    protected void setNgwVersion()
    {
        Pair<Integer, Integer> ver = null;
        try {
            ver = NGWUtil.getNgwVersion(mURL, mLogin, mPassword);
        } catch (IOException | JSONException | NumberFormatException ignored) { }

        if (null != ver) {
            mNgwVersionMajor = ver.first;
            mNgwVersionMinor = ver.second;
        }
    }


    public int getNgwVersionMajor()
    {
        return mNgwVersionMajor;
    }


    public int getNgwVersionMinor()
    {
        return mNgwVersionMinor;
    }


    public String getCookie()
    {
        return mCookie;
    }


    public String getURL()
    {
        return mURL;
    }


    protected void fillCapabilities()
    {
        mSupportedTypes.clear();
        try {
            String sURL = mURL + "/resource/schema";
            HttpResponse response = NetworkUtil.get(sURL, getLogin(), getPassword(), false);
            if (!response.isOk())
                return;
            JSONObject schema = new JSONObject(response.getResponseBody());
            JSONObject resources = schema.getJSONObject("resources");
            if (null != resources) {
                Iterator<String> keys = resources.keys();
                while (keys.hasNext()) {
                    int type = getType(keys.next());
                    if (type != NGWResourceTypeNone) {
                        if (mSupportedTypes.isEmpty()) {
                            mSupportedTypes.add(type);
                        } else if (!isTypeSupported(type)) {
                            mSupportedTypes.add(type);
                        }
                    }
                }
            }
        } catch (IOException | JSONException e) {
            e.printStackTrace();
        }
    }


    public boolean isTypeSupported(int type)
    {
        return mSupportedTypes.isEmpty() || mSupportedTypes.contains(type);
    }


    public int getType(String sType)
    {
        switch (sType) {
            case "resource_group":
                return NGWResourceTypeResourceGroup;
            case "postgis_layer":
                return NGWResourceTypePostgisLayer;
            case "wmsserver_service":
                return NGWResourceTypeWMSServerService;
            case "baselayers":
                return NGWResourceTypeBaseLayers;
            case "postgis_connection":
                return NGWResourceTypePostgisConnection;
            case "webmap":
                return NGWResourceTypeWebMap;
            case "wfsserver_service":
                return NGWResourceTypeWFSServerService;
            case "vector_layer":
                return NGWResourceTypeVectorLayer;
            case "raster_layer":
                return NGWResourceTypeRasterLayer;
            case "file_bucket":
                return NGWResourceTypeFileSet;
            case "wmsclient_layer":
                return NGWResourceTypeWMSClient;
            case "lookup_table":
                return NGWResourceTypeLookupTable;
            default:
                return NGWResourceTypeNone;
        }
    }


    public ResourceGroup getRootResource()
    {
        return mRootResource;
    }


    @Override
    public String getName()
    {
        return mName;
    }


    @Override
    public int getType()
    {
        return NGWResourceTypeConnection;
    }


    @Override
    public int getId()
    {
        return mId;
    }


    @Override
    public INGWResource getResourceById(int id)
    {
        if (id == mId) {
            return this;
        }
        if (null == mRootResource) //not connected
        {
            return null;
        }
        return mRootResource.getResourceById(id);
    }


    @Override
    public int getChildrenCount()
    {
        if (null == mRootResource) {
            return 0;
        }
        return mRootResource.getChildrenCount();
    }


    @Override
    public INGWResource getChild(int i)
    {
        if (null == mRootResource) {
            return null;
        }
        return mRootResource.getChild(i);
    }


    @Override
    public INGWResource getParent()
    {
        return mParent;
    }


    @Override
    public void setParent(INGWResource resource)
    {
        mParent = resource;
    }

    @Override
    public String getKey() {
        return "";
    }


    public void loadChildren()
    {
        if (null != mRootResource) {
            mRootResource.loadChildren();
        }
    }


    @Override
    public int describeContents()
    {
        return 0;
    }


    @Override
    public void writeToParcel(
            Parcel parcel,
            int i)
    {
        parcel.writeString(mName);
        parcel.writeString(mLogin);
        parcel.writeString(mPassword);
        parcel.writeString(mURL);
        parcel.writeByte(mIsConnected ? (byte) 1 : (byte) 0);
        parcel.writeString(mCookie);
        parcel.writeInt(mId);
        parcel.writeInt(mSupportedTypes.size());
        for (Integer type : mSupportedTypes) {
            parcel.writeInt(type);
        }
        parcel.writeParcelable(mRootResource, i);
    }


    public static final Parcelable.Creator<Connection> CREATOR =
            new Parcelable.Creator<Connection>()
            {
                public Connection createFromParcel(Parcel in)
                {
                    return new Connection(in);
                }


                public Connection[] newArray(int size)
                {
                    return new Connection[size];
                }
            };


    protected Connection(Parcel in)
    {
        mName = in.readString();
        mLogin = in.readString();
        mPassword = in.readString();
        mURL = in.readString();
        mIsConnected = in.readByte() == 1;
        mCookie = in.readString();
        mId = in.readInt();
        int count = in.readInt();
        mSupportedTypes = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            mSupportedTypes.add(in.readInt());
        }
        mRootResource = in.readParcelable(ResourceGroup.class.getClassLoader());
        if (null != mRootResource) {
            mRootResource.setConnection(this);
            mRootResource.setParent(this);
        }
    }


    public boolean isConnected()
    {
        return mIsConnected;
    }


    public String getLogin()
    {
        return mLogin;
    }


    public String getPassword()
    {
        return mPassword;
    }
} public class FieldStyleRule implements IStyleRule, IJSONStore {
    public static final String JSON_RULES_KEY = "rules";
    public static final String JSON_FIELD_KEY = "field";

    VectorLayer mLayer;
    Map<String, Style> mStyleRules;
    String mKey;

    public FieldStyleRule(VectorLayer layer) {
        mLayer = layer;
        mStyleRules = new TreeMap<>();
    }

    public void setKey(String key) {
        mKey = key;
    }

    public String getKey() {
        return mKey;
    }

    public void setStyle(String value, Style style) {
        mStyleRules.put(value, style);
    }

    public Style getStyle(String value) {
        return mStyleRules.get(value);
    }

    public void removeStyle(String value) {
        mStyleRules.remove(value);
    }

    public Map<String, Style> getStyleRules() {
        return mStyleRules;
    }

    @Override
    public void setStyleParams(Style style, long featureId) {
        if (mKey == null)
            return;

        Feature feature = mLayer.getFeature(featureId);
        String value = mKey.equals(Constants.FIELD_ID) ? feature.getId() + "" : feature.getFieldValueAsString(mKey);

        if (value != null) {
            Style rule = mStyleRules.get(value);
            if (rule == null)
                return;

            if (style instanceof SimpleMarkerStyle) {
                SimpleMarkerStyle markerStyle = (SimpleMarkerStyle) style;
                SimpleMarkerStyle ruleStyle = (SimpleMarkerStyle) rule;
                markerStyle.setColor(ruleStyle.getColor());
                markerStyle.setOutColor(ruleStyle.getOutColor());
                markerStyle.setType(ruleStyle.getType());
                markerStyle.setSize(ruleStyle.getSize());
                markerStyle.setWidth(ruleStyle.getWidth());
                markerStyle.setText(ruleStyle.getText());
                markerStyle.setField(ruleStyle.getField());
            } else if (style instanceof SimpleLineStyle) {
                SimpleLineStyle lineStyle = (SimpleLineStyle) style;
                SimpleLineStyle ruleStyle = (SimpleLineStyle) rule;
                lineStyle.setColor(ruleStyle.getColor());
                lineStyle.setOutColor(ruleStyle.getOutColor());
                lineStyle.setType(ruleStyle.getType());
                lineStyle.setWidth(ruleStyle.getWidth());
                lineStyle.setText(ruleStyle.getText());
                lineStyle.setField(ruleStyle.getField());
            } else if (style instanceof SimplePolygonStyle) {
                SimplePolygonStyle polygonStyle = (SimplePolygonStyle) style;
                SimplePolygonStyle ruleStyle = (SimplePolygonStyle) rule;
                polygonStyle.setColor(ruleStyle.getColor());
                polygonStyle.setWidth(ruleStyle.getWidth());
                polygonStyle.setFill(ruleStyle.isFill());
                polygonStyle.setText(ruleStyle.getText());
                polygonStyle.setTextSize(ruleStyle.getTextSize());
                polygonStyle.setField(ruleStyle.getField());
            }
        }
    }

    @Override
    public JSONObject toJSON() throws JSONException {
        JSONObject result = new JSONObject();
        JSONArray rules = new JSONArray();
        for (Map.Entry<String, Style> rule : mStyleRules.entrySet()) {
            JSONObject item = new JSONObject();
            item.put(Constants.JSON_VALUE_KEY, rule.getKey());
            item.put(SimpleFeatureRenderer.JSON_STYLE_KEY, rule.getValue().toJSON());
            rules.put(item);
        }

        result.put(JSON_FIELD_KEY, mKey);
        result.put(JSON_RULES_KEY, rules);

        return result;
    }

    @Override
    public void fromJSON(JSONObject jsonObject) throws JSONException {
        if (jsonObject.has(JSON_FIELD_KEY))
            mKey = jsonObject.getString(JSON_FIELD_KEY);

        if (jsonObject.has(JSON_RULES_KEY)) {
            JSONArray rules = jsonObject.getJSONArray(JSON_RULES_KEY);
            try {
                for (int i = 0; i < rules.length(); i++) {
                    JSONObject ruleObject = rules.getJSONObject(i);
                    AtomicReference<Style> reference = new AtomicReference<>();
                    SimpleFeatureRenderer.fromJSON(ruleObject, reference);

                    if (reference.get() != null) {
                        String value = ruleObject.getString(Constants.JSON_VALUE_KEY);
                        mStyleRules.put(value, reference.get());
                    }
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

    public void clearRules() {
        mStyleRules.clear();
    }

    public int size() {
        return mStyleRules.size();
    }
} public class RuleFeatureRenderer
        extends SimpleFeatureRenderer
{
    protected IStyleRule mStyleRule;


    public RuleFeatureRenderer(Layer layer)
    {
        super(layer);
        mStyleRule = null;
    }


    public RuleFeatureRenderer(
            Layer layer,
            IStyleRule styleRule)
    {
        super(layer);
        mStyleRule = styleRule;
    }


    public RuleFeatureRenderer(
            Layer layer,
            IStyleRule styleRule,
            Style style)
    {
        super(layer, style);
        mStyleRule = styleRule;
    }


    @Override
    protected Style getStyle(long featureId)
    {
        if (null == mStyleRule) {
            return super.getStyle(featureId);
        }

        try {
            Style styleClone = mStyle.clone();
            mStyleRule.setStyleParams(styleClone, featureId);
            applyField(styleClone, featureId);
            return styleClone;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return super.getStyle(featureId);
        }
    }


    public IStyleRule getStyleRule()
    {
        return mStyleRule;
    }


    public void setStyleRule(IStyleRule styleRule)
    {
        mStyleRule = styleRule;
    }


    @Override
    public JSONObject toJSON()
            throws JSONException
    {
        JSONObject rootJsonObject = super.toJSON();
        rootJsonObject.put(JSON_NAME_KEY, "RuleFeatureRenderer");

        if (mStyleRule instanceof IJSONStore)
            rootJsonObject.put(JSON_STYLE_RULE_KEY, ((IJSONStore) mStyleRule).toJSON());

        return rootJsonObject;
    }
} public class SimpleFeatureRenderer
        extends Renderer
{

    protected Style              mStyle;
    protected ThreadPoolExecutor mDrawThreadPool;
    //protected final Object lock = new Object();

    public static final String JSON_STYLE_KEY = "style";
    protected static final int GEOMETRY_PER_TASK = 15;


    public SimpleFeatureRenderer(Layer layer)
    {
        super(layer);
        mStyle = null;
    }


    public SimpleFeatureRenderer(
            Layer layer,
            Style style)
    {
        super(layer);
        mStyle = style;
    }

    @Override
    public void runDraw(final GISDisplay display)
    {
        long startTime;
        if(Constants.DEBUG_MODE) {
            startTime = System.currentTimeMillis();
        }

        if (null == mStyle) {
            Log.d(TAG, "mStyle == null");
            return;
        }
        final double zoom = display.getZoomLevel();

        GeoEnvelope env = display.getBounds();

        final VectorLayer vectorLayer = (VectorLayer) getLayer();

        //GeoEnvelope layerEnv = vectorLayer.getExtents();
        //if (null == layerEnv || !env.intersects(layerEnv)) {
        //    return;
        //}

        int decimalZoom = (int) zoom;
        if(decimalZoom % 2 != 0)
            decimalZoom++;

        List<Long> featureIds = vectorLayer.query(env);

        cancelDraw();

        int threadCount = DRAWING_SEPARATE_THREADS;
        int coreCount = Runtime.getRuntime().availableProcessors();

        // FIXME more than 1 pool size causing strange behaviour on 6.0
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
            coreCount = 1;

        mDrawThreadPool = new ThreadPoolExecutor(
                coreCount, threadCount, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT,
                new LinkedBlockingQueue<Runnable>(), new RejectedExecutionHandler()
        {
            @Override
            public void rejectedExecution(
                    Runnable r,
                    ThreadPoolExecutor executor)
            {
                try {
                    executor.getQueue().put(r);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                }
            }
        });

        if(Constants.DEBUG_MODE) {
            long stopTime = System.currentTimeMillis();
            long elapsedTime = stopTime - startTime;

            Log.d(TAG, "Vector layer " + vectorLayer.getName() + " prepare time: " + elapsedTime);
        }

        // http://developer.android.com/reference/java/util/concurrent/ExecutorCompletionService.html
        int tilesSize = featureIds.size() / GEOMETRY_PER_TASK + 1;
        List<Future> futures = new ArrayList<>(tilesSize);

        final int finalDecimalZoom = decimalZoom;
        int counter = 0;
        for (int i = 0; i < featureIds.size(); i += GEOMETRY_PER_TASK) {

            DrawTask task = new DrawTask(finalDecimalZoom, vectorLayer, display);

            for(int j = 0; j < GEOMETRY_PER_TASK; j++) {
                if (Thread.currentThread().isInterrupted()) {
                    break;
                }
                if (counter >= featureIds.size())
                    break;

                final long featureId = featureIds.get(counter);

                task.addTaskData(featureId);
                counter++;
            }

            futures.add(mDrawThreadPool.submit(task));
            vectorLayer.onDrawFinished(vectorLayer.getId(), 0.01f);
        }

        // wait for draw ending
        int nStep = futures.size() / Constants.DRAW_NOTIFY_STEP_PERCENT;
        if(nStep == 0)
            nStep = 1;
        for (int i = 0, futuresSize = futures.size(); i < futuresSize; i++) {
            if (Thread.currentThread().isInterrupted()) {
                break;
            }

            try {
                Future future = futures.get(i);
                future.get(); // wait for task ending

                float percent = (float) i / futuresSize;
                if(i % nStep == 0) //0..10..20..30..40..50..60..70..80..90..100
                    vectorLayer.onDrawFinished(vectorLayer.getId(), percent);

                //Log.d(TAG, "TMS percent: " + percent + " complete: " + i +
                //       " tiles count: " + tilesSize + " layer: " + mLayer.getName());

            } catch (CancellationException | InterruptedException e) {
                //e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

        vectorLayer.onDrawFinished(vectorLayer.getId(), 1.0f);

        if(Constants.DEBUG_MODE) {
            long stopTime = System.currentTimeMillis();
            long elapsedTime = stopTime - startTime;

            Log.d(TAG, "Vector layer " + vectorLayer.getName() + " exec time: " + elapsedTime);
        }
    }


    /**
     * If subclass's getStyle(long featureId) changes style params then must be so in the method
     * body:
     * <pre> {@code
     * try {
     *     Style styleClone = mStyle.clone();
     *     // changing styleClone params
     *     return styleClone;
     * } catch (CloneNotSupportedException e) {
     *     e.printStackTrace();
     *     return mStyle;
     *     // or treat this situation instead
     * }
     * } </pre>
     * or
     * <pre> {@code
     * Style style = new Style(); // or from subclass of Style
     * // changing style params
     * return style;
     * } </pre>
     */
    protected Style getStyle(long featureId)
    {
        applyField(mStyle, featureId);
        return mStyle;
    }


    protected Style applyField(Style style, long featureId) {
        if (style instanceof ITextStyle) {
            String fieldValue = ((ITextStyle) style).getField();

            if (fieldValue != null) {
                Feature feature = ((VectorLayer) getLayer()).getFeature(featureId);
                if (fieldValue.equals(FIELD_ID))
                    fieldValue = feature.getId() + "";
                else
                    fieldValue = feature.getFieldValueAsString(fieldValue);

                ((ITextStyle) style).setText(fieldValue);
            }
        }

        return style;
    }


    @Override
    public void cancelDraw()
    {
        if (mDrawThreadPool != null) {
            //synchronized (lock) {
                mDrawThreadPool.shutdownNow();
            //}
            try {
                mDrawThreadPool.awaitTermination(TERMINATE_TIME, KEEP_ALIVE_TIME_UNIT);
                //mDrawThreadPool.purge();
            } catch (InterruptedException e) {
                //e.printStackTrace();
            }
        }
    }


    public Style getStyle()
    {
        return mStyle;
    }


    @Override
    public JSONObject toJSON()
            throws JSONException
    {
        JSONObject rootJsonObject = new JSONObject();
        rootJsonObject.put(JSON_NAME_KEY, "SimpleFeatureRenderer");

        if (null != mStyle) {
            rootJsonObject.put(JSON_STYLE_KEY, mStyle.toJSON());
        }

        return rootJsonObject;
    }


    @Override
    public void fromJSON(JSONObject jsonObject)
            throws JSONException
    {
        AtomicReference<Style> reference = new AtomicReference<>();
        fromJSON(jsonObject, reference);
        mStyle = reference.get();

    }

    public static void fromJSON(JSONObject jsonObject, AtomicReference<Style> style) throws JSONException {
        JSONObject styleJsonObject = jsonObject.getJSONObject(JSON_STYLE_KEY);
        String styleName = styleJsonObject.getString(JSON_NAME_KEY);
        switch (styleName) {
            case "SimpleMarkerStyle":
                style.set(new SimpleMarkerStyle());
                break;
            case "SimpleLineStyle":
                style.set(new SimpleLineStyle());
                break;
            case "SimplePolygonStyle":
                style.set(new SimplePolygonStyle());
                break;
            case "SimpleTiledPolygonStyle":
                style.set(new SimpleTiledPolygonStyle());
                break;
            default:
                throw new JSONException("Unknown style type: " + styleName);
        }
        style.get().fromJSON(styleJsonObject);
    }

    protected class DrawTask implements Runnable {
        protected final GISDisplay mDisplay;
        protected final int mZoom;
        protected final VectorLayer mLayer;
        protected final List<Long> mFeatureIds = new ArrayList<>(GEOMETRY_PER_TASK);

        public DrawTask(final int zoom, final VectorLayer layer, final GISDisplay display) {
            mDisplay = display;
            mZoom = zoom;
            mLayer = layer;
        }

        public void addTaskData(final Long featureId){
            mFeatureIds.add(featureId);
        }

        @Override
        public void run() {
            android.os.Process.setThreadPriority(
                    Constants.DEFAULT_DRAW_THREAD_PRIORITY);

            MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
            SQLiteDatabase db = map.getDatabase(true);

            for(Long id : mFeatureIds) {
                if(mLayer.isFeatureHidden(id))
                    continue;
                final GeoGeometry geometry = mLayer.getGeometryForId(id, mZoom, db);
                if (geometry != null) {
                    final Style style = getStyle(id);
                    style.onDraw(geometry, mDisplay);
                }
            }
        }
    }
} public class SimpleLineStyle extends Style implements ITextStyle {
    public final static int LineStyleSolid = 1;
    public final static int LineStyleDash = 2;
    public final static int LineStyleEdgingSolid = 3;

    protected int mType;
    protected Paint.Cap mStrokeCap;
    protected String mField;
    protected String mText;

    public SimpleLineStyle() {
        super();
        mStrokeCap = Paint.Cap.BUTT;
    }

    public SimpleLineStyle(int fillColor, int outColor, int type) {
        super(fillColor, outColor);
        mType = type;
        mStrokeCap = Paint.Cap.BUTT;
    }

    @Override
    public SimpleLineStyle clone() throws CloneNotSupportedException {
        SimpleLineStyle obj = (SimpleLineStyle) super.clone();
        obj.mType = mType;
        obj.mStrokeCap = mStrokeCap;
        obj.mText = mText;
        obj.mField = mField;
        return obj;
    }

    public void onDraw(GeoLineString lineString, GISDisplay display) {
        if (null == lineString) {
            return;
        }

        float scaledWidth = (float) (mWidth / display.getScale());
        Path mainPath = null;
        switch (mType) {
            case LineStyleSolid:
                mainPath = drawSolidLine(scaledWidth, lineString, display);
                break;

            case LineStyleDash:
                mainPath = drawDashLine(scaledWidth, lineString, display);
                break;

            case LineStyleEdgingSolid:
                mainPath = drawSolidEdgingLine(scaledWidth, lineString, display);
                break;
        }

        drawText(scaledWidth, mainPath, display);
    }

    protected void drawText(float scaledWidth, Path mainPath, GISDisplay display) {
        if (TextUtils.isEmpty(mText) || mainPath == null)
            return;

        Paint textPaint = new Paint();
        textPaint.setColor(mOutColor);
        textPaint.setAntiAlias(true);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setStrokeCap(Paint.Cap.ROUND);
        textPaint.setStrokeWidth(scaledWidth);

        float textSize = 12 * scaledWidth;
        textPaint.setTextSize(textSize);
        float textWidth = textPaint.measureText(mText);
        float vOffset = (float) (textSize / 2.7);

        // draw text along the main path
        PathMeasure pm = new PathMeasure(mainPath, false);
        float length = pm.getLength();
        float gap = textPaint.measureText("_");
        float period = textWidth + gap;
        float startD = gap;
        float stopD = startD + period;

        Path textPath = new Path();

        while (stopD < length) {
            textPath.reset();
            pm.getSegment(startD, stopD, textPath, true);
            textPath.rLineTo(0, 0); // workaround for API <= 19

            display.drawTextOnPath(mText, textPath, 0, vOffset, textPaint);

            startD += period;
            stopD += period;
        }

        stopD = startD;
        float rest = length - stopD;

        if (rest > gap * 2) {
            stopD = length - gap;

            textPath.reset();
            pm.getSegment(startD, stopD, textPath, true);
            textPath.rLineTo(0, 0); // workaround for API <= 19

            display.drawTextOnPath(mText, textPath, 0, vOffset, textPaint);
        }
    }

    @Override
    public void onDraw(GeoGeometry geoGeometry, GISDisplay display) {
        mColor = Color.argb(mInnerAlpha, Color.red(mColor), Color.green(mColor), Color.blue(mColor));
        mOutColor = Color.argb(mOuterAlpha, Color.red(mOutColor), Color.green(mOutColor), Color.blue(mOutColor));

        switch (geoGeometry.getType()) {
            case GTLineString:
                onDraw((GeoLineString) geoGeometry, display);
                break;

            case GTMultiLineString:
                GeoMultiLineString multiLineString = (GeoMultiLineString) geoGeometry;
                for (int i = 0; i < multiLineString.size(); i++) {
                    onDraw(multiLineString.get(i), display);
                }
                break;

            //throw new IllegalArgumentException(
            //        "The input geometry type is not support by this style");
        }

    }

    protected Path drawSolidLine(float scaledWidth, GeoLineString lineString, GISDisplay display) {
        Paint paint = new Paint();
        paint.setColor(mColor);
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeCap(mStrokeCap);
        paint.setStrokeWidth(scaledWidth);

        List<GeoPoint> points = lineString.getPoints();

        Path path = new Path();
        path.incReserve(points.size());

        path.moveTo((float) points.get(0).getX(), (float) points.get(0).getY());

        for (int i = 1; i < points.size(); ++i) {
            path.lineTo((float) points.get(i).getX(), (float) points.get(i).getY());
        }

        display.drawPath(path, paint);

        return path;
    }

    protected Path drawDashLine(float scaledWidth, GeoLineString lineString, GISDisplay display) {
        Paint paint = new Paint();
        paint.setColor(mColor);
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeCap(Paint.Cap.BUTT);
        paint.setStrokeWidth(scaledWidth);

        List<GeoPoint> points = lineString.getPoints();

        // workaround for "DashPathEffect/drawLine not working properly when hardwareAccelerated="true""
        // https://code.google.com/p/android/issues/detail?id=29944

        // get all points to the main path
        Path mainPath = new Path();
        mainPath.incReserve(points.size());

        mainPath.moveTo((float) points.get(0).getX(), (float) points.get(0).getY());

        for (int i = 1; i < points.size(); ++i) {
            mainPath.lineTo((float) points.get(i).getX(), (float) points.get(i).getY());
        }

        // draw along the main path
        PathMeasure pm = new PathMeasure(mainPath, false);
        float[] coordinates = new float[2];
        float length = pm.getLength();
        float dash = (float) (10 / display.getScale());
        float gap = (float) (5 / display.getScale());
        float distance = dash;
        boolean isDash = true;

        Path dashPath = new Path();
        dashPath.incReserve((int) (2 * length / (dash + gap)));

        dashPath.moveTo((float) points.get(0).getX(), (float) points.get(0).getY());

        while (distance < length) {
            // get a point from the main path
            pm.getPosTan(distance, coordinates, null);

            if (isDash) {
                dashPath.lineTo(coordinates[0], coordinates[1]);
                distance += gap;
            } else {
                dashPath.moveTo(coordinates[0], coordinates[1]);
                distance += dash;
            }

            isDash = !isDash;
        }

        // add a rest from the main path
        if (isDash) {
            distance = distance - dash;
            float rest = length - distance;

            if (rest > (float) (1 / display.getScale())) {
                distance = length - 1;
                pm.getPosTan(distance, coordinates, null);
                dashPath.lineTo(coordinates[0], coordinates[1]);
            }
        }

        display.drawPath(dashPath, paint);

        return mainPath;
    }

    protected Path drawSolidEdgingLine(float scaledWidth, GeoLineString lineString, GISDisplay display) {
        Paint mainPaint = new Paint();
        mainPaint.setColor(mColor);
        mainPaint.setAntiAlias(true);
        mainPaint.setStyle(Paint.Style.STROKE);
        mainPaint.setStrokeCap(Paint.Cap.BUTT);
        mainPaint.setStrokeWidth(scaledWidth);

        Paint edgingPaint = new Paint(mainPaint);
        edgingPaint.setColor(mOutColor);
        edgingPaint.setStrokeCap(Paint.Cap.BUTT);
        edgingPaint.setStrokeWidth(scaledWidth * 3);

        List<GeoPoint> points = lineString.getPoints();

        Path path = new Path();
        path.incReserve(points.size());

        path.moveTo((float) points.get(0).getX(), (float) points.get(0).getY());

        for (int i = 1; i < points.size(); ++i) {
            path.lineTo((float) points.get(i).getX(), (float) points.get(i).getY());
        }

        display.drawPath(path, edgingPaint);
        display.drawPath(path, mainPaint);

        return path;
    }

    public int getType() {
        return mType;
    }

    public void setType(int type) {
        mType = type;
    }

    public String getField() {
        return mField;
    }

    public void setField(String field) {
        mField = field;
    }

    public String getText() {
        return mText;
    }

    public void setText(String text) {
        if (!TextUtils.isEmpty(text))
            mText = text;
        else
            mText = null;
    }

    @Override
    public JSONObject toJSON() throws JSONException {
        JSONObject rootConfig = super.toJSON();
        rootConfig.put(JSON_NAME_KEY, "SimpleLineStyle");
        rootConfig.put(JSON_TYPE_KEY, mType);

        if (null != mText) {
            rootConfig.put(JSON_DISPLAY_NAME, mText);
        }
        if (null != mField) {
            rootConfig.put(JSON_VALUE_KEY, mField);
        }

        return rootConfig;
    }

    @Override
    public void fromJSON(JSONObject jsonObject) throws JSONException {
        super.fromJSON(jsonObject);
        mType = jsonObject.getInt(JSON_TYPE_KEY);

        if (jsonObject.has(JSON_DISPLAY_NAME)) {
            mText = jsonObject.getString(JSON_DISPLAY_NAME);
        }
        if (jsonObject.has(JSON_VALUE_KEY)) {
            mField = jsonObject.getString(JSON_VALUE_KEY);
        }
    }
} public class SimpleMarkerStyle extends Style implements ITextStyle {
    public final static int MarkerStylePoint = 1;
    public final static int MarkerStyleCircle = 2;
    public final static int MarkerStyleDiamond = 3;
    public final static int MarkerStyleCross = 4;
    public final static int MarkerStyleTriangle = 5;
    public final static int MarkerStyleBox = 6;
    public final static int MarkerEditStyleCircle = 7;
    public final static int MarkerStyleCrossedBox = 8;

    public final static float SIZE_SMALL = 3;
    public final static float SIZE_MEDIUM = 6;
    public final static float SIZE_BIG = 10;
    public final static ArrayList<Float> SIZES = new ArrayList<>(Arrays.asList(new Float[]{SIZE_SMALL, SIZE_MEDIUM, SIZE_BIG}));

    public final static int ALIGN_TOP = 0;
    public final static int ALIGN_TOP_RIGHT = 1;
    public final static int ALIGN_RIGHT = 2;
    public final static int ALIGN_BOTTOM_RIGHT = 3;
    public final static int ALIGN_BOTTOM = 4;
    public final static int ALIGN_BOTTOM_LEFT = 5;
    public final static int ALIGN_LEFT = 6;
    public final static int ALIGN_TOP_LEFT = 7;
    public final static ArrayList<Integer> ALIGNMENTS = new ArrayList<>(Arrays.asList(
            new Integer[]{ALIGN_TOP, ALIGN_TOP_RIGHT, ALIGN_RIGHT, ALIGN_BOTTOM_RIGHT, ALIGN_BOTTOM, ALIGN_BOTTOM_LEFT, ALIGN_LEFT, ALIGN_TOP_LEFT}));

    protected int mType;
    protected float mSize, mTextSize = 3;
    protected Paint mOutPaint;
    protected Paint mFillPaint;
    protected String mField;
    protected String mText;
    protected int mTextAlignment;
    protected int mTextColor = Color.BLACK;

    public SimpleMarkerStyle() {
        super();
        initPaints();
    }

    public SimpleMarkerStyle(int fillColor, int outColor, float size, int type) {
        super(fillColor, outColor);

        mType = type;
        mSize = size;
        mWidth = 1;

        initPaints();
        setPaintsColors();
    }

    @Override
    public SimpleMarkerStyle clone() throws CloneNotSupportedException {
        SimpleMarkerStyle obj = (SimpleMarkerStyle) super.clone();
        obj.mType = mType;
        obj.mSize = mSize;
        obj.mTextSize = mTextSize;
        obj.mTextAlignment = mTextAlignment;
        obj.mTextColor = mTextColor;
        obj.mText = mText;
        obj.mField = mField;
        return obj;
    }

    protected void initPaints() {
        mFillPaint = new Paint();
        mFillPaint.setStrokeCap(Paint.Cap.ROUND);

        mOutPaint = new Paint();
        mOutPaint.setStyle(Paint.Style.STROKE);
        mOutPaint.setAntiAlias(true);
    }

    protected void setPaintsColors() {
        mFillPaint.setColor(mColor);
        mOutPaint.setColor(mOutColor);
    }

    protected void onDraw(GeoPoint pt, GISDisplay display) {
        if (null == pt) {
            return;
        }

        mColor = Color.argb(mInnerAlpha, Color.red(mColor), Color.green(mColor), Color.blue(mColor));
        mOutColor = Color.argb(mOuterAlpha, Color.red(mOutColor), Color.green(mOutColor), Color.blue(mOutColor));
        mFillPaint.setColor(mColor);
        mOutPaint.setColor(mOutColor);
        float scaledSize = (float) (mSize / display.getScale());
        float width = (float) (mWidth / display.getScale());
        switch (mType) {
            case MarkerStylePoint:
                drawPointMarker(scaledSize, pt, display);
                break;
            case MarkerStyleCircle:
                drawCircleMarker(scaledSize, width, pt, display);
                break;
            case MarkerStyleDiamond:
                drawDiamondMarker(scaledSize, width, pt, display);
                break;
            case MarkerStyleCross:
                drawCrossMarker(scaledSize, width, pt, display);
                break;
            case MarkerStyleTriangle:
                drawTriangleMarker(scaledSize, width, pt, display);
                break;
            case MarkerStyleBox:
                drawBoxMarker(scaledSize, width, pt, display);
                break;
            case MarkerStyleCrossedBox:
                drawCrossedBoxMarker(scaledSize, width, pt, display);
                break;
        }

//        drawText(scaledSize - width, pt, display);
        drawText(scaledSize, mTextSize, pt, display);
    }

    protected void drawText(float radius, float size, GeoPoint pt, GISDisplay display) {
        if (TextUtils.isEmpty(mText))
            return;

        Paint textPaint = new Paint();
        textPaint.setColor(mTextColor);
        textPaint.setAntiAlias(true);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setStrokeCap(Paint.Cap.ROUND);

        float gap = (float) (1 / display.getScale());
        float innerRadius = radius - gap;
        float textSize = size * innerRadius; // initial text size

        Rect textRect = new Rect();
        textPaint.setTextSize(size);
        textPaint.getTextBounds(mText, 0, mText.length(), textRect);
        textPaint.setTextSize(textSize);

        float halfW = textRect.width() * innerRadius / 2;
        float halfH = textRect.height() * innerRadius / 2;
//        float outerTextRadius = (float) Math.sqrt(halfH * halfH + halfW * halfW);
//        float textScale = innerRadius / outerTextRadius;

        float textX = (float) (pt.getX() - halfW - radius / 2);
        float textY = (float) (pt.getY() + halfH - radius / 2);

        switch (mTextAlignment) {
            case ALIGN_TOP:
                textY = textY - halfH * 2;
                break;
            case ALIGN_TOP_RIGHT:
                textX = textX + halfW * 2 - halfW * .5f;
                textY = textY - halfH * 2;
                break;
            case ALIGN_RIGHT:
                textX = textX + halfW * 2 - halfW * 0.25f;
                break;
            case ALIGN_BOTTOM_RIGHT:
                textX = textX + halfW * 2 - halfW * .5f;
                textY = textY + halfH * 2;
                break;
            case ALIGN_BOTTOM:
                textY = textY + halfH * 2;
                break;
            case ALIGN_BOTTOM_LEFT:
                textX = textX - halfW * 2 + halfW * .5f;
                textY = textY + halfH * 2;
                break;
            case ALIGN_LEFT:
                textX = textX - halfW * 2 + halfW * 0.25f;
                break;
            case ALIGN_TOP_LEFT:
                textX = textX - halfW * 2 + halfW * .5f;
                textY = textY - halfH * 2;
                break;
        }
        Path textPath = new Path();
        textPaint.getTextPath(mText, 0, mText.length(), textX, textY, textPath);
        textPath.close();

        Matrix matrix = new Matrix();
        matrix.reset();
        matrix.setScale(1, -1, (float) pt.getX(), (float) pt.getY());
        textPath.transform(matrix);

        display.drawPath(textPath, textPaint);
    }

    @Override
    public void onDraw(GeoGeometry geoGeometry, GISDisplay display) {
        switch (geoGeometry.getType()) {
            case GTPoint:
                GeoPoint pt = (GeoPoint) geoGeometry;
                onDraw(pt, display);
                break;
            case GTMultiPoint:
                GeoMultiPoint multiPoint = (GeoMultiPoint) geoGeometry;
                for (int i = 0; i < multiPoint.size(); i++) {
                    onDraw(multiPoint.get(i), display);
                }
                break;

            //throw new IllegalArgumentException(
            //        "The input geometry type is not support by this style");
        }
    }

    protected void drawPointMarker(float scaledSize, GeoPoint pt, GISDisplay display) {
        mOutPaint.setColor(mColor);
        mOutPaint.setStrokeWidth(scaledSize);
        display.drawPoint((float) pt.getX(), (float) pt.getY(), mOutPaint);
        mOutPaint.setColor(mOutColor);
    }

    protected void drawCircleMarker(float scaledSize, float width, GeoPoint pt, GISDisplay display) {
        if (scaledSize < 2) {
            mOutPaint.setColor(mColor);
            mOutPaint.setStrokeWidth(scaledSize);
            display.drawCircle((float) pt.getX(), (float) pt.getY(), mOutPaint);
        } else
            display.drawCircle((float) pt.getX(), (float) pt.getY(), scaledSize, mFillPaint);

        mOutPaint.setStrokeWidth(width);
        if (scaledSize >= 2) {
            mOutPaint.setColor(mOutColor);
            display.drawCircle((float) pt.getX(), (float) pt.getY(), scaledSize, mOutPaint);
        }
    }

    protected void drawDiamondMarker(float scaledSize, float width, GeoPoint pt, GISDisplay display) {
        Path path = new Path();
        path.moveTo((float) pt.getX() + scaledSize, (float) pt.getY());
        path.lineTo((float) pt.getX(), (float) pt.getY() + scaledSize);
        path.lineTo((float) pt.getX() - scaledSize, (float) pt.getY());
        path.lineTo((float) pt.getX(), (float) pt.getY() - scaledSize);
        path.close();

        drawPath(width, path, display);
    }

    protected void drawTriangleMarker(float scaledSize, float width, GeoPoint pt, GISDisplay display) {
        Path path = new Path();
        path.moveTo((float) pt.getX() + scaledSize, (float) pt.getY() - scaledSize);
        path.lineTo((float) pt.getX(), (float) pt.getY() + scaledSize);
        path.lineTo((float) pt.getX() - scaledSize, (float) pt.getY() - scaledSize);
        path.close();

        drawPath(width, path, display);
    }

    protected void drawPath(float width, Path path, GISDisplay display) {
        display.drawPath(path, mFillPaint);
        mOutPaint.setStrokeWidth(width);
        display.drawPath(path, mOutPaint);
    }

    protected void drawBoxMarker(float scaledSize, float width, GeoPoint pt, GISDisplay display) {
        display.drawBox((float) pt.getX(), (float) pt.getY(), scaledSize, mFillPaint);
        mOutPaint.setStrokeWidth(width);
        display.drawBox((float) pt.getX(), (float) pt.getY(), scaledSize, mOutPaint);
    }

    protected void drawCrossMarker(float scaledSize, float width, GeoPoint pt, GISDisplay display) {
        mOutPaint.setStrokeWidth(width);
        display.drawCross((float) pt.getX(), (float) pt.getY(), scaledSize, mOutPaint);
    }

    protected void drawCrossedBoxMarker(float scaledSize, float width, GeoPoint pt, GISDisplay display) {
        display.drawBox((float) pt.getX(), (float) pt.getY(), scaledSize, mFillPaint);
        mOutPaint.setStrokeWidth(width);
        display.drawCrossedBox((float) pt.getX(), (float) pt.getY(), scaledSize, mOutPaint);
    }

    public int getType() {
        return mType;
    }

    public void setType(int type) {
        mType = type;
    }

    public float getSize() {
        return mSize;
    }

    public void setSize(float size) {
        mSize = size;
    }

    public float getTextSize() {
        return mTextSize;
    }

    public void setTextSize(float size) {
        mTextSize = size;
    }

    public int getTextAlignment() {
        return mTextAlignment;
    }

    public void setTextAlignment(int alignment) {
        mTextAlignment = alignment;
    }

    public int getTextColor() {
        return mTextColor;
    }

    public void setTextColor(int color) {
        mTextColor = color;
    }

    @Override
    public void setColor(int color) {
        super.setColor(color);
        setPaintsColors();
    }

    public String getField() {
        return mField;
    }

    public void setField(String field) {
        mField = field;
    }

    public String getText() {
        return mText;
    }

    public void setText(String text) {
        if (!TextUtils.isEmpty(text))
            mText = text;
        else
            mText = null;
    }

    @Override
    public JSONObject toJSON() throws JSONException {
        JSONObject rootConfig = super.toJSON();
        rootConfig.put(JSON_NAME_KEY, "SimpleMarkerStyle");
        rootConfig.put(JSON_TYPE_KEY, mType);
        rootConfig.put(JSON_SIZE_KEY, mSize);
        rootConfig.put(JSON_TEXT_SIZE_KEY, mTextSize);
        rootConfig.put(JSON_TEXT_ALIGN_KEY, mTextAlignment);
        rootConfig.put(JSON_TEXT_COLOR_KEY, mTextColor);

        if (null != mText) {
            rootConfig.put(JSON_DISPLAY_NAME, mText);
        }
        if (null != mField) {
            rootConfig.put(JSON_VALUE_KEY, mField);
        }

        return rootConfig;
    }

    @Override
    public void fromJSON(JSONObject jsonObject) throws JSONException {
        super.fromJSON(jsonObject);
        mType = jsonObject.getInt(JSON_TYPE_KEY);
        mSize = (float) jsonObject.getDouble(JSON_SIZE_KEY);
        mTextSize = (float) jsonObject.getDouble(JSON_TEXT_SIZE_KEY);
        mTextAlignment = jsonObject.getInt(JSON_TEXT_ALIGN_KEY);
        mTextColor = jsonObject.getInt(JSON_TEXT_COLOR_KEY);

        if (jsonObject.has(JSON_DISPLAY_NAME)) {
            mText = jsonObject.getString(JSON_DISPLAY_NAME);
        }
        if (jsonObject.has(JSON_VALUE_KEY)) {
            mField = jsonObject.getString(JSON_VALUE_KEY);
        }

        setPaintsColors();
    }
} public class SimplePolygonStyle extends Style implements ITextStyle {
    protected boolean mFill;
    protected String mField;
    protected String mText;
    protected Float mTextSize = 12f;

    protected static final String JSON_FILL_KEY = "fill";

    public SimplePolygonStyle() {
        super();
    }

    public SimplePolygonStyle(int color, int outColor) {
        super(color, outColor);
        mOuterAlpha = 128;
        mInnerAlpha = 64;
        mWidth = 3;
        mFill = true;
    }

    @Override
    public SimplePolygonStyle clone() throws CloneNotSupportedException {
        SimplePolygonStyle obj = (SimplePolygonStyle) super.clone();
        obj.mFill = mFill;
        obj.mText = mText;
        obj.mTextSize = mTextSize;
        obj.mField = mField;
        return obj;
    }

    public boolean isFill() {
        return mFill;
    }

    public void setFill(boolean fill) {
        mFill = fill;
    }

    @Override
    public void onDraw(GeoGeometry geoGeometry, GISDisplay display) {
        Float textSize = (null == mTextSize) ? null : 12f;
        float scaledTextSize = (float) (mTextSize * (mWidth / display.getScale()));
        GeoPoint center = geoGeometry.getEnvelope().getCenter();
        switch (geoGeometry.getType()) {
            case GTPolygon:
                drawPolygon((GeoPolygon) geoGeometry, display);
                drawText(scaledTextSize, center, display);
                break;
            case GTMultiPolygon:
                GeoMultiPolygon multiPolygon = (GeoMultiPolygon) geoGeometry;

                for (int i = 0; i < multiPolygon.size(); i++) {
                    drawPolygon(multiPolygon.get(i), display);
                    drawText(scaledTextSize, center, display);
                }
                break;

            //throw new IllegalArgumentException(
            //        "The input geometry type is not support by this style");
        }
    }

    protected void drawText(Float scaledTextSize, GeoPoint center, GISDisplay display) {
        if (TextUtils.isEmpty(mText) || null == scaledTextSize) { return; }

        Paint textPaint = new Paint();
        textPaint.setColor(Color.BLACK);
        textPaint.setAntiAlias(true);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setStrokeCap(Paint.Cap.ROUND);
        textPaint.setAlpha(128);

        Rect textRect = new Rect();
        textPaint.setTextSize(scaledTextSize);
        textPaint.getTextBounds(mText, 0, mText.length(), textRect);

        float halfW = textRect.width() / 2;
        float halfH = textRect.height() / 2;

        float textX = (float) (center.getX() - halfW);
        float textY = (float) (center.getY() + halfH);

        Path textPath = new Path();
        textPaint.getTextPath(mText, 0, mText.length(), textX, textY, textPath);
        textPath.close();

        Matrix matrix = new Matrix();
        matrix.reset();
        matrix.setScale(1, -1, (float) center.getX(), (float) center.getY());
        textPath.transform(matrix);

        display.drawPath(textPath, textPaint);
    }

    public void drawPolygon(GeoPolygon polygon, GISDisplay display) {
        float scaledWidth = (float) (mWidth / display.getScale());

        Paint lnPaint = new Paint();
        lnPaint.setColor(mOutColor);
        lnPaint.setStrokeWidth(scaledWidth);
        lnPaint.setStrokeCap(Paint.Cap.ROUND);
        lnPaint.setAntiAlias(true);

        Path polygonPath = getPath(polygon);

        lnPaint.setStyle(Paint.Style.STROKE);
        lnPaint.setAlpha(mOuterAlpha);
        display.drawPath(polygonPath, lnPaint);

        if (mFill) {
            lnPaint.setStyle(Paint.Style.FILL);
            lnPaint.setColor(mColor);
            lnPaint.setAlpha(mInnerAlpha);
            display.drawPath(polygonPath, lnPaint);
        }
    }

    public String getField() {
        return mField;
    }

    public void setField(String field) {
        mField = field;
    }

    public String getText() {
        return mText;
    }

    public void setText(String text) {
        if (!TextUtils.isEmpty(text))
            mText = text;
        else
            mText = null;
    }

    public Float getTextSize() {
        return mTextSize;
    }

    public void setTextSize(Float textSize) {
        mTextSize = textSize;
    }

    @Override
    public JSONObject toJSON() throws JSONException {
        JSONObject rootConfig = super.toJSON();
        rootConfig.put(JSON_FILL_KEY, mFill);
        rootConfig.put(JSON_NAME_KEY, "SimplePolygonStyle");

        if (null != mText) {
            rootConfig.put(JSON_DISPLAY_NAME, mText);
        }
        if (null != mTextSize) {
            rootConfig.put(JSON_TEXT_SIZE_KEY, mTextSize);
        }
        if (null != mField) {
            rootConfig.put(JSON_VALUE_KEY, mField);
        }

        return rootConfig;
    }

    @Override
    public void fromJSON(JSONObject jsonObject) throws JSONException {
        super.fromJSON(jsonObject);
        mFill = jsonObject.optBoolean(JSON_FILL_KEY, true);

        if (jsonObject.has(JSON_DISPLAY_NAME)) {
            mText = jsonObject.getString(JSON_DISPLAY_NAME);
        }
        if (jsonObject.has(JSON_TEXT_SIZE_KEY)) {
            mTextSize = (float) jsonObject.optDouble(JSON_TEXT_SIZE_KEY, 12);
        }
        if (jsonObject.has(JSON_VALUE_KEY)) {
            mField = jsonObject.getString(JSON_VALUE_KEY);
        }
    }

    protected Path getPath(GeoPolygon polygon) {
        List<GeoPoint> points = polygon.getOuterRing().getPoints();
        Path polygonPath = new Path();
        appendPath(polygonPath, points);

        for (int i = 0; i < polygon.getInnerRingCount(); i++) {
            points = polygon.getInnerRing(i).getPoints();
            appendPath(polygonPath, points);
        }

        polygonPath.setFillType(Path.FillType.EVEN_ODD);

        return polygonPath;
    }

    protected void appendPath(Path polygonPath, List<GeoPoint> points) {
        float x0, y0;

        if (points.size() > 0) {
            x0 = (float) points.get(0).getX();
            y0 = (float) points.get(0).getY();
            polygonPath.moveTo(x0, y0);

            for (int i = 1; i < points.size(); i++) {
                x0 = (float) points.get(i).getX();
                y0 = (float) points.get(i).getY();

                polygonPath.lineTo(x0, y0);
            }

            polygonPath.close();
        }
    }
} public abstract class Style implements IJSONStore, Cloneable {
    protected float mWidth;
    protected int mColor;
    protected int mOutColor;
    protected int mOuterAlpha = 255;
    protected int mInnerAlpha = 255;

    public Style() {
        mWidth = 3;
    }

    public Style clone() throws CloneNotSupportedException {
        Style obj = (Style) super.clone();
        obj.mWidth = mWidth;
        obj.mColor = mColor;
        obj.mOutColor = mOutColor;
        obj.mInnerAlpha = mInnerAlpha;
        obj.mOuterAlpha = mOuterAlpha;
        return obj;
    }

    public int getColor() {
        return mColor;
    }

    public void setColor(int color) {
        mColor = color;
    }

    public int getOutColor() {
        return mOutColor;
    }

    public void setOutColor(int outColor) {
        mOutColor = outColor;
    }

    public float getWidth() {
        return mWidth;
    }

    public void setWidth(float width) {
        mWidth = width;
    }

    public Style(final int color, int outColor) {
        this();
        mColor = color;
        mOutColor = outColor;
    }

    public abstract void onDraw(GeoGeometry geoGeometry, GISDisplay display);

    @Override
    public JSONObject toJSON() throws JSONException {
        JSONObject rootConfig = new JSONObject();
        rootConfig.put(JSON_WIDTH_KEY, mWidth);
        rootConfig.put(JSON_COLOR_KEY, mColor);
        rootConfig.put(JSON_OUTCOLOR_KEY, mOutColor);
        rootConfig.put(JSON_ALPHA_KEY, mInnerAlpha);
        rootConfig.put(JSON_OUTALPHA_KEY, mOuterAlpha);
        return rootConfig;
    }

    @Override
    public void fromJSON(JSONObject jsonObject) throws JSONException {
        mWidth = (float) jsonObject.optDouble(JSON_WIDTH_KEY, 3);
        mColor = jsonObject.getInt(JSON_COLOR_KEY);
        mOutColor = jsonObject.optInt(JSON_OUTCOLOR_KEY, mColor);
        mInnerAlpha = jsonObject.optInt(JSON_ALPHA_KEY, 255);
        mOuterAlpha = jsonObject.optInt(JSON_OUTALPHA_KEY, 255);
    }

    public int getAlpha() {
        return mInnerAlpha;
    }

    public void setAlpha(int alpha) {
        mInnerAlpha = alpha;
    }

    public int getOutAlpha() {
        return mOuterAlpha;
    }

    public void setOutAlpha(int alpha) {
        mOuterAlpha = alpha;
    }
} public class AttachItem implements IJSONStore {
    String mDescription;
    String mDisplayName;
    String mMimetype;
    String mAttachId;
    int mSize;

    protected static final String JSON_ID_KEY = "id";
    protected static final String JSON_DESCRIPTION_KEY = "desc";
    protected static final String JSON_MIME_KEY = "mime";
    protected static final String JSON_DISPLAY_NAME_KEY = "display_name";
    protected static final String JSON_SIZE_KEY = Constants.JSON_SIZE_KEY;

    public AttachItem() {
    }

    public AttachItem(String attachId, String displayName, String mimetype, String description) {
        mDescription = description;
        mDisplayName = displayName;
        mMimetype = mimetype;
        mAttachId = attachId;
    }

    public AttachItem(String attachId, String displayName, String mimetype, String description, int size) {
        mDescription = description;
        mDisplayName = displayName;
        mMimetype = mimetype;
        mAttachId = attachId;
        mSize = size;
    }

    @Override
    public JSONObject toJSON() throws JSONException {
        JSONObject out = new JSONObject();
        out.put(JSON_ID_KEY, mAttachId);
        out.put(JSON_DESCRIPTION_KEY, mDescription);
        out.put(JSON_MIME_KEY, mMimetype);
        out.put(JSON_DISPLAY_NAME_KEY, mDisplayName);
        out.put(JSON_SIZE_KEY, mSize);
        return out;
    }

    @Override
    public void fromJSON(JSONObject jsonObject) throws JSONException {
        mAttachId = jsonObject.getString(JSON_ID_KEY);
        mDisplayName = jsonObject.getString(JSON_DISPLAY_NAME_KEY);
        mMimetype = jsonObject.getString(JSON_MIME_KEY);
        mDescription = jsonObject.getString(JSON_DESCRIPTION_KEY);
        mSize = jsonObject.optInt(JSON_SIZE_KEY);
    }

    public String getDescription() {
        return mDescription;
    }

    public String getDisplayName() {
        return mDisplayName;
    }

    public String getMimetype() {
        return mMimetype;
    }

    public String getAttachId() {
        return mAttachId;
    }

    public int getSize() {
        return mSize;
    }

    public void setDescription(String description) {
        mDescription = description;
    }

    public void setDisplayName(String displayName) {
        mDisplayName = displayName;
    }

    public void setMimetype(String mimetype) {
        mMimetype = mimetype;
    }

    public void setAttachId(String attachId) {
        mAttachId = attachId;
    }

    public void setSize(int size) {
        mSize = size;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        AttachItem that = (AttachItem) o;

        return that.mSize == mSize && mAttachId.equals(that.mAttachId) && !(mDescription != null ? !mDescription
                .equals(that.mDescription) : that.mDescription != null) && !(mDisplayName != null ? !mDisplayName
                .equals(that.mDisplayName) : that.mDisplayName != null) && !(mMimetype != null ? !mMimetype.equals(that.mMimetype) : that.mMimetype != null);

    }

    @Override
    public int hashCode() {
        return mAttachId.hashCode();
    }

    public ContentValues getContentValues(boolean withAttachId) {
        ContentValues returnValues = new ContentValues();
        returnValues.put(VectorLayer.ATTACH_DISPLAY_NAME, mDisplayName);
        returnValues.put(VectorLayer.ATTACH_MIME_TYPE, mMimetype);
        returnValues.put(VectorLayer.ATTACH_DESCRIPTION, mDescription);
        returnValues.put(VectorLayer.ATTACH_SIZE, mSize);

        if (withAttachId) {
            returnValues.put(VectorLayer.ATTACH_ID, mAttachId);
        }

        return returnValues;
    }
} public interface Constants
{
    String  TAG                = "nextgismobile";
    String  PREFERENCES        = "nextgismobile";
    String  SUPPORT            = "support.bin";
    int     NOT_FOUND          = -1;
    boolean DEBUG_MODE         = false;

    int NGW_v3 = 3;

    /**
     * HTTP parameters
     */
    String APP_USER_AGENT     = "NextGIS Mobile maplib v" + BuildConfig.VERSION_NAME;
    int    IO_BUFFER_SIZE     = 32 * 1024; // 32k
    int    MAX_CONTENT_LENGTH = 5 * 1024 * 1024; //5Mb

    long MIN_LOCAL_FEATURE_ID = 10000000;
    int  MAX_TILES_COUNT      = 6001;

    /**
     * NGW account type
     */
    String NGW_ACCOUNT_TYPE = "com.nextgis.account";
    String NGW_ACCOUNT_GUEST = "anonymous";

    /**
     * Map parameters
     */
    float MIN_SCROLL_STEP = 5.5f;
    int   MAP_LIMITS_NO   = 1; // no limits to scroll map
    int   MAP_LIMITS_X    = 2; // limit to scroll map by x axis
    int   MAP_LIMITS_Y    = 3; // limit to scroll map by y axis
    int   MAP_LIMITS_XY   = 4; // limit to scroll map by x & y axis

    /**
     * The additional size to off screen drawing from 1 and higher As more than more memory needed
     */

    float OFFSCREEN_EXTRASIZE_RATIO = 1.0f;
    int   DEFAULT_TILE_SIZE         = 256;

    /**
     * thread priorities and delays
     */
    int DEFAULT_DRAW_THREAD_PRIORITY       = android.os.Process.THREAD_PRIORITY_DEFAULT + 11;
    int DEFAULT_DOWNLOAD_THREAD_PRIORITY   = android.os.Process.THREAD_PRIORITY_BACKGROUND + 3;
    int DEFAULT_LOAD_LAYER_THREAD_PRIORITY = Thread.MIN_PRIORITY;
    int DEFAULT_EXECUTION_DELAY            = 650;

    /**
     * tune line string and linear ring simplifier
     */
    double SIMPLIFY_TOENV_AREA_MULTIPLY = 1.5;
    // area multiplier to skip if greater than quad tolerance
    double SIMPLIFY_SKIP_AREA_MULTIPLY  = 5;
    int    SAMPLE_DISTANCE_PX           = 5;


    String CONFIG       = "config.json";
    String LAYER_PREFIX = "layer_";
    String MAP_EXT      = ".ngm";

    /**
     * notifications
     */
    String NOTIFY_DELETE            = "com.nextgis.maplib.notify_delete";
    String NOTIFY_DELETE_ALL        = "com.nextgis.maplib.notify_delete_all";
    String NOTIFY_INSERT            = "com.nextgis.maplib.notify_insert";
    String NOTIFY_UPDATE            = "com.nextgis.maplib.notify_update";
    String NOTIFY_UPDATE_ALL        = "com.nextgis.maplib.notify_update_all";
    String NOTIFY_UPDATE_FIELDS     = "com.nextgis.maplib.notify_update_fields";
    String NOTIFY_FEATURE_ID_CHANGE = "com.nextgis.maplib.notify_change_id";

    String NOTIFY_LAYER_NAME = "layer_name";

    /**
     * JSON keys
     */
    String JSON_ID_KEY            = "id";
    String JSON_NAME_KEY          = "name";
    String JSON_VISIBILITY_KEY    = "visible";
    String JSON_LEVELS_KEY        = "levels";
    String JSON_LEVEL_KEY         = "level";
    String JSON_TYPE_KEY          = "type";
    String JSON_MAXLEVEL_KEY      = "max_level";
    String JSON_MINLEVEL_KEY      = "min_level";
    String JSON_LAYERS_KEY        = "layers";
    String JSON_LAYER_KEY         = "layer";
    String JSON_PATH_KEY          = "path";
    String JSON_BBOX_MINX_KEY     = "bbox_minx";
    String JSON_BBOX_MINY_KEY     = "bbox_miny";
    String JSON_BBOX_MAXX_KEY     = "bbox_maxx";
    String JSON_BBOX_MAXY_KEY     = "bbox_maxy";
    String JSON_RENDERERPROPS_KEY = "renderer_properties";
    String JSON_STYLE_RULE_KEY    = "style_rule";
    String JSON_WIDTH_KEY         = "width";
    String JSON_COLOR_KEY         = "color";
    String JSON_OUTCOLOR_KEY      = "out_color";
    String JSON_ALPHA_KEY         = "alpha";
    String JSON_OUTALPHA_KEY      = "out_alpha";
    String JSON_CHANGES_KEY       = "changes";
    String JSON_VALUE_KEY         = "value";
    String JSON_SIZE_KEY          = "size";
    String JSON_TEXT_SIZE_KEY     = "text_size";
    String JSON_TEXT_ALIGN_KEY    = "text_alignment";
    String JSON_TEXT_COLOR_KEY    = "text_color";
    String JSON_DISPLAY_NAME      = "display_name";
    String JSON_RESOURCE_KEY      = "resource";
    String JSON_MESSAGE_KEY       = "message";
    String JSON_EXTENT_KEY        = "extent";
    String JSON_MIN_LAT_KEY       = "minLat";
    String JSON_MAX_LAT_KEY       = "maxLat";
    String JSON_MIN_LON_KEY       = "minLon";
    String JSON_MAX_LON_KEY       = "maxLon";
    String JSON_SUPPORTED_KEY     = "supported";
    String JSON_START_DATE_KEY    = "start_date";
    String JSON_END_DATE_KEY      = "end_date";
    String JSON_USER_ID_KEY       = "nextgis_guid";
    String JSON_SIGNATURE_KEY     = "sign";

    /**
     * database fields
     */
    String FIELD_ID               = "_id";
    String FIELD_OLD_ID           = "old_id";
    String FIELD_GEOM             = "_geom";
    String FIELD_GEOM_            = "_geom_";
    String FIELD_FEATURE_ID       = "feature_id";
    String FIELD_OPERATION        = "operation";
    String FIELD_ATTACH_ID        = "attach_id";
    String FIELD_ATTACH_OPERATION = "attach_operation";

    String ATTRIBUTES_ONLY = "attributes_only";

    /**
     * Layer type
     */
    String CE50 = "CE50";
    String CE90 = "CE90";
    String CE95 = "CE95";
    String CE98 = "CE98";

    /**
     * Layer type
     */
    int LAYERTYPE_REMOTE_TMS   = 1 << 0;
    int LAYERTYPE_NGW_RASTER   = 1 << 1;
    int LAYERTYPE_NGW_VECTOR   = 1 << 2;
    int LAYERTYPE_GROUP        = 1 << 3;
    int LAYERTYPE_LOCAL_VECTOR = 1 << 4;
    int LAYERTYPE_LOCAL_TMS    = 1 << 5;
    int LAYERTYPE_TRACKS       = 1 << 6;
    int LAYERTYPE_LOOKUPTABLE  = 1 << 7;
    int LAYERTYPE_NGW_WEBMAP   = 1 << 8;

    int LAYERTYPE_SYSMAX = 9; // should be the max + 1 of system layer type

    /**
     * File type
     */
    int FILETYPE_PARENT  = 1 << 0;
    int FILETYPE_FOLDER  = 1 << 1;
    int FILETYPE_ZIP     = 1 << 2;
    int FILETYPE_GEOJSON = 1 << 3;
    int FILETYPE_FB      = 1 << 4;
    int FILETYPE_UNKNOWN = 1 << 31;

    /**
     * time constants
     */
    long     ONE_SECOND           = 1000;
    long     ONE_MINUTE           = ONE_SECOND * 60;
    long     ONE_HOUR             = ONE_MINUTE * 60;
    long     ONE_DAY              = ONE_HOUR * 24;
    long     ONE_WEEK             = ONE_DAY * 7;
    long     DEFAULT_TILE_MAX_AGE = ONE_WEEK;
    long     ONE_YEAR             = ONE_DAY * 365;
    int      KEEP_ALIVE_TIME      = 35;
    int      TERMINATE_TIME       = 350;
    TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.MILLISECONDS;

    int  SYNC_NONE           = 1 << 0;
    int  SYNC_GEOMETRY       = 1 << 1;
    int  SYNC_ATTRIBUTES     = 1 << 2;
    int  SYNC_DATA           = SYNC_GEOMETRY | SYNC_ATTRIBUTES;
    int  SYNC_ATTACH         = 1 << 3;
    int  SYNC_ALL            = SYNC_DATA | SYNC_ATTACH;
    long DEFAULT_SYNC_PERIOD = 3600; //1 hour

    String URI_ATTACH  = "attach";
    String URI_CHANGES = "changes";

    // http://stackoverflow.com/a/24055457
    String URI_PARAMETER_LIMIT    = "limit";
    String URI_PARAMETER_TEMP     = "temp";
    String URI_PARAMETER_NOT_SYNC = "not_sync";

    String CHANGES_NAME_POSTFIX      = "_changes";
    int    CHANGE_OPERATION_TEMP     = 1;
    int    CHANGE_OPERATION_NEW      = 1 << 1; // 2
    int    CHANGE_OPERATION_CHANGED  = 1 << 2; // 4
    int    CHANGE_OPERATION_DELETE   = 1 << 3; // 8
    int    CHANGE_OPERATION_ATTACH   = 1 << 4; // 16
    int    CHANGE_OPERATION_NOT_SYNC = 1 << 5; // 32

    int DRAWING_SEPARATE_THREADS = 9;
    int DRAW_NOTIFY_STEP_PERCENT = 10;
    int DRAW_FINISH_ID = -22;

    String[] VECTOR_FORBIDDEN_FIELDS = {
            "ABORT",
            "ACTION",
            "ADD",
            "AFTER",
            "ALL",
            "ALTER",
            "ANALYZE",
            "AND",
            "AS",
            "ASC",
            "ATTACH",
            "AUTOINCREMENT",
            "BEFORE",
            "BEGIN",
            "BETWEEN",
            "BY",
            "CASCADE",
            "CASE",
            "CAST",
            "CHECK",
            "COLLATE",
            "COLUMN",
            "COMMIT",
            "CONFLICT",
            "CONSTRAINT",
            "CREATE",
            "CROSS",
            "CURRENT_DATE",
            "CURRENT_TIME",
            "CURRENT_TIMESTAMP",
            "DATABASE",
            "DEFAULT",
            "DEFERRABLE",
            "DEFERRED",
            "DELETE",
            "DESC",
            "DETACH",
            "DISTINCT",
            "DROP",
            "EACH",
            "ELSE",
            "END",
            "ESCAPE",
            "EXCEPT",
            "EXCLUSIVE",
            "EXISTS",
            "EXPLAIN",
            "FAIL",
            "FOR",
            "FOREIGN",
            "FROM",
            "FULL",
            "GLOB",
            "GROUP",
            "HAVING",
            "IF",
            "IGNORE",
            "IMMEDIATE",
            "IN",
            "INDEX",
            "INDEXED",
            "INITIALLY",
            "INNER",
            "INSERT",
            "INSTEAD",
            "INTERSECT",
            "INTO",
            "IS",
            "ISNULL",
            "JOIN",
            "KEY",
            "LEFT",
            "LIKE",
            "LIMIT",
            "MATCH",
            "NATURAL",
            "NO",
            "NOT",
            "NOTNULL",
            "NULL",
            "OF",
            "OFFSET",
            "ON",
            "OR",
            "ORDER",
            "OUTER",
            "PLAN",
            "PRAGMA",
            "PRIMARY",
            "QUERY",
            "RAISE",
            "RECURSIVE",
            "REFERENCES",
            "REGEXP",
            "REINDEX",
            "RELEASE",
            "RENAME",
            "REPLACE",
            "RESTRICT",
            "RIGHT",
            "ROLLBACK",
            "ROW",
            "SAVEPOINT",
            "SELECT",
            "SET",
            "TABLE",
            "TEMP",
            "TEMPORARY",
            "THEN",
            "TO",
            "TRANSACTION",
            "TRIGGER",
            "UNION",
            "UNIQUE",
            "UPDATE",
            "USING",
            "VACUUM",
            "VALUES",
            "VIEW",
            "VIRTUAL",
            "WHEN",
            "WHERE",
            "WITH",
            "WITHOUT"};

} public class FeatureChanges
{
    public static void initialize(String tableName)
    {
        Log.d(TAG, "init the change log for the layer " + tableName);

        String sqlCreateTable = "CREATE TABLE IF NOT EXISTS " + tableName + " ( ";
        sqlCreateTable += FIELD_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, ";
        sqlCreateTable += FIELD_FEATURE_ID + " INTEGER, ";
        sqlCreateTable += FIELD_OPERATION + " INTEGER, ";
        sqlCreateTable += FIELD_ATTACH_ID + " INTEGER, ";
        sqlCreateTable += FIELD_ATTACH_OPERATION + " INTEGER";
        sqlCreateTable += " );";

        Log.d(TAG, "create the layer change table: " + sqlCreateTable);

        // create table
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        SQLiteDatabase db = map.getDatabase(true);
        db.execSQL(sqlCreateTable);
    }


    public static Cursor query(
            String tableName,
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder,
            String limit)

    {
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        SQLiteDatabase db = map.getDatabase(true);

        try {
            return db.query(
                    tableName, projection, selection, selectionArgs, null, null, sortOrder, limit);
        } catch (SQLiteException e) {
            Log.d(TAG, e.getLocalizedMessage());
            return null;
        }
    }


    public static Cursor query(
            String tableName,
            String selection,
            String sortOrder,
            String limit)
    {
        return query(tableName, null, selection, null, sortOrder, limit);
    }


    public static long insert(
            String tableName,
            ContentValues values)
    {
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        SQLiteDatabase db = map.getDatabase(false);
        return db.insert(tableName, null, values);
    }


    public static long replace(
            String tableName,
            ContentValues values)
    {
        long featureId = values.getAsLong(FIELD_FEATURE_ID);
        int featureOperation = values.getAsInteger(FIELD_OPERATION);
        long attachId = values.getAsLong(FIELD_ATTACH_ID);
        int attachOperation = values.getAsInteger(FIELD_ATTACH_OPERATION);

        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                FIELD_OPERATION + " = " + featureOperation + " AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                FIELD_ATTACH_OPERATION + " = " + attachOperation;

        Cursor cursor = query(tableName, selection, null, "1");
        long res = 0;

        if (null != cursor) {
            res = cursor.getCount();
            cursor.close();
        }

        if (res > 0) {
            return res;
        }

        return insert(tableName, values);
    }


    public static int update(
            String tableName,
            ContentValues values,
            String selection,
            String[] selectionArgs)
    {
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        SQLiteDatabase db = map.getDatabase(true);
        return db.update(tableName, values, selection, selectionArgs);
    }


    public static int delete(
            String tableName,
            String selection,
            String[] selectionArgs)
    {
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        SQLiteDatabase db = map.getDatabase(true);
        int retResult = 0;
        try {
            retResult = db.delete(tableName, selection, selectionArgs);
        } catch (SQLiteException e) {
            e.printStackTrace();
            Log.d(TAG, e.getLocalizedMessage());
        }
        return retResult;
    }


    public static int delete(
            String tableName,
            String selection)
    {
        return delete(tableName, selection, null);
    }


    public static void delete(String tableName)
    {
        try {
            MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
            SQLiteDatabase db = map.getDatabase(true);
            String tableDrop = "DROP TABLE IF EXISTS " + tableName;
            db.execSQL(tableDrop);
        } catch (SQLiteFullException | SQLiteReadOnlyDatabaseException e) {
            e.printStackTrace();
        }
    }


    public static boolean isRecords(
            String tableName,
            String selection)
    {
        Cursor cursor = query(tableName, selection, null, "1");
        boolean ret = false;

        if (null != cursor) {
            if (cursor.getCount() > 0) {
                ret = true;
            }
            cursor.close();
        }

        return ret;
    }


    public static long getEntriesCount(String tableName)
    {
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        SQLiteDatabase db = map.getDatabase(true);

        try {
            return DatabaseUtils.queryNumEntries(db, tableName);
        } catch (SQLiteException e) {
            e.printStackTrace();
            Log.d(TAG, e.getLocalizedMessage());
            return 0;
        }
    }


    protected static String getSelectionForSync()
    {
        return "( " +

                "0 == " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " AND " +
                "0 == " + FIELD_OPERATION + " & " + CHANGE_OPERATION_TEMP +
                " AND " +
                "0 == " + FIELD_OPERATION + " & " + CHANGE_OPERATION_NOT_SYNC +

                " OR " +

                "0 != " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " AND " +
                "0 == " + FIELD_ATTACH_OPERATION + " & " + CHANGE_OPERATION_TEMP +
                " AND " +
                "0 == " + FIELD_ATTACH_OPERATION + " & " + CHANGE_OPERATION_NOT_SYNC +

                " )";
    }


    public static long getChangeCount(String tableName)
    {
        String selection = getSelectionForSync();
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        SQLiteDatabase db = map.getDatabase(true);

        try {
            // From sources of DatabaseUtils.queryNumEntries()
            String s = (!TextUtils.isEmpty(selection)) ? " where " + selection : "";
            return DatabaseUtils.longForQuery(db, "select count(*) from " + tableName + s, null);
        } catch (SQLiteException e) {
            e.printStackTrace();
            Log.d(TAG, e.getLocalizedMessage());
            return 0;
        }
    }


    public static Cursor getFirstChangeFromRecordId(
            String tableName,
            long recordId)
    {
        String sortOrder = FIELD_ID + " ASC";
        String selection = FIELD_ID + " >= " + recordId + " AND " + getSelectionForSync();
        return query(tableName, selection, sortOrder, "1");
    }


    public static long getLastChangeRecordId(String tableName)
    {
        String sortOrder = FIELD_ID + " DESC";
        String selection = getSelectionForSync();
        Cursor cursor = query(tableName, selection, sortOrder, "1");
        long ret = NOT_FOUND;

        if (null == cursor) {
            return ret;
        }

        try {
            if (cursor.moveToFirst()) {
                ret = cursor.getLong(cursor.getColumnIndex(FIELD_ID));
            }
        } catch (Exception e) {
            //Log.d(TAG, e.getLocalizedMessage());
        } finally {
            cursor.close();
        }

        return ret;
    }


    public static Cursor getChanges(String tableName)
    {
        String sortOrder = FIELD_ID + " ASC";
        String selection = getSelectionForSync();
        return query(tableName, selection, sortOrder, null);
    }


    public static boolean isChanges(String tableName)
    {
        String selection = getSelectionForSync();
        return isRecords(tableName, selection);
    }


    public static Cursor getChanges(
            String tableName,
            long featureId)
    {
        String sortOrder = FIELD_ID + " ASC";
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " + getSelectionForSync();
        return query(tableName, selection, sortOrder, null);
    }


    public static boolean isChanges(
            String tableName,
            long featureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " + getSelectionForSync();
        return isRecords(tableName, selection);
    }


    public static Cursor getChanges(
            String tableName,
            long featureId,
            int operation)
    {
        String sortOrder = FIELD_ID + " ASC";
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + operation + " ) )" + " AND "
                + getSelectionForSync();

        return query(tableName, selection, sortOrder, null);
    }


    public static boolean isChanges(
            String tableName,
            long featureId,
            int operation)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + operation + " ) )" + " AND "
                + getSelectionForSync();

        return isRecords(tableName, selection);
    }


    public static Cursor getAttachChanges(
            String tableName,
            long featureId)
    {
        String sortOrder = FIELD_ID + " ASC";
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH + " ) )" + " AND "
                + getSelectionForSync();

        return query(tableName, selection, sortOrder, null);
    }


    public static boolean isAttachChanges(
            String tableName,
            long featureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH + " ) )" + " AND "
                + getSelectionForSync();

        return isRecords(tableName, selection);
    }


    public static Cursor getAttachChanges(
            String tableName,
            long featureId,
            long attachId)
    {
        String sortOrder = FIELD_ID + " ASC";
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                getSelectionForSync();

        return query(tableName, selection, sortOrder, null);
    }


    public static boolean isAttachChanges(
            String tableName,
            long featureId,
            long attachId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                getSelectionForSync();

        return isRecords(tableName, selection);
    }


    public static Cursor getAttachChanges(
            String tableName,
            long featureId,
            long attachId,
            int attachOperation)
    {
        String sortOrder = FIELD_ID + " ASC";
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + attachOperation + " ) )" + " AND "
                + getSelectionForSync();

        return query(tableName, selection, sortOrder, null);
    }


    public static boolean isAttachChanges(
            String tableName,
            long featureId,
            long attachId,
            int attachOperation)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + attachOperation + " ) )" + " AND " +
                getSelectionForSync();

        return isRecords(tableName, selection);
    }


    public static boolean isAttachesForDelete(
            String tableName,
            long featureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + CHANGE_OPERATION_DELETE +
                " ) )" + " AND " +
                getSelectionForSync();

        return isRecords(tableName, selection);
    }


    public static long add(
            String tableName,
            long featureId,
            int operation)
    {
        ContentValues values = new ContentValues();
        values.put(FIELD_FEATURE_ID, featureId);
        values.put(FIELD_OPERATION, operation);
        values.put(FIELD_ATTACH_ID, NOT_FOUND);
        values.put(FIELD_ATTACH_OPERATION, 0);

        return insert(tableName, values);
    }


    public static long add(
            String tableName,
            long featureId,
            long attachId,
            int attachOperation)
    {
        ContentValues values = new ContentValues();
        values.put(FIELD_FEATURE_ID, featureId);
        values.put(FIELD_OPERATION, CHANGE_OPERATION_ATTACH);
        values.put(FIELD_ATTACH_ID, attachId);
        values.put(FIELD_ATTACH_OPERATION, attachOperation);

        return insert(tableName, values);
    }


    public static int setOperation(
            String tableName,
            long recordId,
            int operation)
    {
        String selection = FIELD_ID + " = " + recordId;

        ContentValues values = new ContentValues();
        values.put(FIELD_OPERATION, operation);

        return update(tableName, values, selection, null);
    }


    public static int setOperation(
            String tableName,
            long recordId,
            long featureId,
            long attachId,
            int attachOperation)
    {
        String selection = FIELD_ID + " = " + recordId + " AND " +
                FIELD_FEATURE_ID + " = " + featureId + " AND " +
                FIELD_ATTACH_ID + " = " + attachId;

        ContentValues values = new ContentValues();
        values.put(FIELD_ATTACH_OPERATION, attachOperation);

        return update(tableName, values, selection, null);
    }


    public static int changeFeatureId(
            String tableName,
            long oldFeatureId,
            long newFeatureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + oldFeatureId;

        ContentValues values = new ContentValues();
        values.put(FIELD_FEATURE_ID, newFeatureId);

        return update(tableName, values, selection, null);
    }


    public static int changeFeatureIdForAttaches(
            String tableName,
            long oldFeatureId,
            long newFeatureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + oldFeatureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) )";

        ContentValues values = new ContentValues();
        values.put(FIELD_FEATURE_ID, newFeatureId);

        return update(tableName, values, selection, null);
    }


    public static int removeAllChanges(String tableName)
    {
        String selection = getSelectionForSync();
        return delete(tableName, selection);
    }


    public static int removeAllChangesToLast(
            String tableName,
            long lastRecordId)
    {
        String selection = FIELD_ID + " <= " + lastRecordId + " AND " + getSelectionForSync();
        return delete(tableName, selection);
    }


    public static int removeChanges(
            String tableName,
            long featureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " + getSelectionForSync();
        return delete(tableName, selection);
    }


    public static int removeChangesToLast(
            String tableName,
            long featureId,
            long lastRecordId)
    {
        String selection = FIELD_ID + " <= " + lastRecordId + " AND " +
                FIELD_FEATURE_ID + " = " + featureId + " AND " +
                getSelectionForSync();

        return delete(tableName, selection);
    }


    public static int removeChanges(
            String tableName,
            long featureId,
            int operation)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + operation + " ) )" + " AND " +
                getSelectionForSync();

        return delete(tableName, selection);
    }


    public static int removeChangesToLast(
            String tableName,
            long featureId,
            int operation,
            long lastRecordId)
    {
        String selection = FIELD_ID + " <= " + lastRecordId + " AND " +
                FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + operation + " ) )" + " AND " +
                getSelectionForSync();

        return delete(tableName, selection);
    }


    public static int removeAllAttachChanges(
            String tableName,
            long featureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH + " ) )" + " AND "
                + getSelectionForSync();

        return delete(tableName, selection);
    }


    public static int removeAllAttachChangesToLast(
            String tableName,
            long featureId,
            long lastRecordId)
    {
        String selection = FIELD_ID + " <= " + lastRecordId + " AND " +
                FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH + " ) )" + " AND "
                + getSelectionForSync();

        return delete(tableName, selection);
    }


    public static int removeAttachChanges(
            String tableName,
            long featureId,
            long attachId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                getSelectionForSync();

        return delete(tableName, selection);
    }


    public static int removeAttachChangesToLast(
            String tableName,
            long featureId,
            long attachId,
            long lastRecordId)
    {
        String selection = FIELD_ID + " <= " + lastRecordId + " AND " +
                FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                getSelectionForSync();

        return delete(tableName, selection);
    }


    public static int removeAttachChanges(
            String tableName,
            long featureId,
            long attachId,
            int attachOperation)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + attachOperation + " ) )" + " AND "
                + getSelectionForSync();

        return delete(tableName, selection);
    }


    public static int removeAttachChangesToLast(
            String tableName,
            long featureId,
            long attachId,
            int attachOperation,
            long lastRecordId)
    {
        String selection = FIELD_ID + " <= " + lastRecordId + " AND " +
                FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + attachOperation + " ) )" + " AND " +
                getSelectionForSync();

        return delete(tableName, selection);
    }


    public static int removeChangeRecord(
            String tableName,
            long recordId)
    {
        String selection = FIELD_ID + " = " + recordId + " AND " + getSelectionForSync();
        return delete(tableName, selection);
    }


    public static boolean hasFeatureFlags(
            String tableName,
            long featureId)
    {
        return hasFeatureTempFlag(tableName, featureId)
                || hasFeatureNotSyncFlag(tableName, featureId);
    }


    public static boolean hasAttachFlags(
            String tableName,
            long featureId,
            long attachId)
    {
        return hasAttachTempFlag(tableName, featureId, attachId)
                || hasAttachNotSyncFlag(tableName, featureId, attachId);
    }


    public static boolean hasFeatureTempFlag(
            String tableName,
            long featureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_TEMP + " ) )";

        Cursor changesCursor = query(tableName, selection, null, "1");

        boolean res = false;
        if (null != changesCursor) {
            res = changesCursor.getCount() > 0;
            changesCursor.close();
        }

        return res;
    }


    public static boolean haveFeaturesNotSyncFlag(String tableName)
    {
        String selection =
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_NOT_SYNC + " ) )";

        Cursor changesCursor = query(tableName, selection, null, "1");

        boolean res = false;
        if (null != changesCursor) {
            res = changesCursor.getCount() > 0;
            changesCursor.close();
        }

        return res;
    }


    public static boolean hasFeatureNotSyncFlag(
            String tableName,
            long featureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_NOT_SYNC + " ) )";

        Cursor changesCursor = query(tableName, selection, null, "1");

        boolean res = false;
        if (null != changesCursor) {
            res = changesCursor.getCount() > 0;
            changesCursor.close();
        }

        return res;
    }


    public static boolean hasAttachTempFlag(
            String tableName,
            long featureId,
            long attachId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + CHANGE_OPERATION_TEMP + " ) )";

        Cursor changesCursor = query(tableName, selection, null, "1");

        boolean res = false;
        if (null != changesCursor) {
            res = changesCursor.getCount() > 0;
            changesCursor.close();
        }

        return res;
    }


    public static boolean hasAttachNotSyncFlag(
            String tableName,
            long featureId,
            long attachId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + CHANGE_OPERATION_NOT_SYNC + " ) )";

        Cursor changesCursor = query(tableName, selection, null, "1");

        boolean res = false;
        if (null != changesCursor) {
            res = changesCursor.getCount() > 0;
            changesCursor.close();
        }

        return res;
    }


    public static long setFeatureTempFlag(
            String tableName,
            long featureId)
    {
        ContentValues values = new ContentValues();
        values.put(FIELD_FEATURE_ID, featureId);
        values.put(FIELD_OPERATION, CHANGE_OPERATION_TEMP);
        values.put(FIELD_ATTACH_ID, NOT_FOUND);
        values.put(FIELD_ATTACH_OPERATION, 0);

        return replace(tableName, values);
    }


    public static long setFeatureNotSyncFlag(
            String tableName,
            long featureId)
    {
        ContentValues values = new ContentValues();
        values.put(FIELD_FEATURE_ID, featureId);
        values.put(FIELD_OPERATION, CHANGE_OPERATION_NOT_SYNC);
        values.put(FIELD_ATTACH_ID, NOT_FOUND);
        values.put(FIELD_ATTACH_OPERATION, 0);

        return replace(tableName, values);
    }


    public static long setAttachTempFlag(
            String tableName,
            long featureId,
            long attachId)
    {
        ContentValues values = new ContentValues();
        values.put(FIELD_FEATURE_ID, featureId);
        values.put(FIELD_OPERATION, CHANGE_OPERATION_ATTACH);
        values.put(FIELD_ATTACH_ID, attachId);
        values.put(FIELD_ATTACH_OPERATION, CHANGE_OPERATION_TEMP);

        return replace(tableName, values);
    }


    public static long setAttachNotSyncFlag(
            String tableName,
            long featureId,
            long attachId)
    {
        ContentValues values = new ContentValues();
        values.put(FIELD_FEATURE_ID, featureId);
        values.put(FIELD_OPERATION, CHANGE_OPERATION_ATTACH);
        values.put(FIELD_ATTACH_ID, attachId);
        values.put(FIELD_ATTACH_OPERATION, CHANGE_OPERATION_NOT_SYNC);

        return replace(tableName, values);
    }


    public static int deleteFeatureTempFlag(
            String tableName,
            long featureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_TEMP + " ) )";

        return delete(tableName, selection);
    }


    public static int deleteFeatureNotSyncFlag(
            String tableName,
            long featureId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_NOT_SYNC + " ) )";

        return delete(tableName, selection);
    }


    public static int deleteAttachTempFlag(
            String tableName,
            long featureId,
            long attachId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + CHANGE_OPERATION_TEMP + " ) )";

        return delete(tableName, selection);
    }


    public static int deleteAttachNotSyncFlag(
            String tableName,
            long featureId,
            long attachId)
    {
        String selection = FIELD_FEATURE_ID + " = " + featureId + " AND " +
                "( 0 != ( " + FIELD_OPERATION + " & " + CHANGE_OPERATION_ATTACH +
                " ) ) AND " +
                FIELD_ATTACH_ID + " = " + attachId + " AND " +
                "( 0 != ( " + FIELD_ATTACH_OPERATION + " & " + CHANGE_OPERATION_NOT_SYNC + " ) )";

        return delete(tableName, selection);
    }
} public class FileUtil
{
    public final static String AUTHORITY = ".easypicker.provider";

    public static boolean isIntegerParseInt(String str)
    {
        try {
            Integer.parseInt(str);
            return true;
        } catch (NumberFormatException nfe) {
            return false;
        }
    }


    public static void writeToFile(
            File filePath,
            String sData)
            throws IOException
    {
        writeToFile(filePath, sData, false);
    }

    public static void writeToFile(
            File filePath,
            String sData,
            boolean append)
            throws IOException
    {
        FileOutputStream os = new FileOutputStream(filePath, append);
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(os);
        outputStreamWriter.write(sData);
        outputStreamWriter.close();
    }


    public static String readFromFile(File filePath)
            throws IOException
    {

        String ret;

        FileInputStream inputStream = new FileInputStream(filePath);
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        String receiveString;
        StringBuilder stringBuilder = new StringBuilder();

        while ((receiveString = bufferedReader.readLine()) != null) {
            stringBuilder.append(receiveString);
        }

        inputStream.close();
        ret = stringBuilder.toString();

        return ret;
    }


    public static synchronized void createDir(File dir) throws RuntimeException
    {
        if (dir.exists()) {
            return;
        }
        if (!dir.mkdirs()) {
            throw new RuntimeException("Can not create dir " + dir);
        }
    }


    // http://stackoverflow.com/a/30574112
    public static boolean renameAndDelete(File fileOrDirectory)
    {
        File newFile = new File(
                fileOrDirectory.getParent() + File.separator + "_" + fileOrDirectory.getName() +
                "_temp_rename");
        return fileOrDirectory.renameTo(newFile) && deleteRecursive(newFile);
    }


    public static boolean deleteRecursive(File fileOrDirectory)
    {
        boolean isOK = true;

        if (fileOrDirectory.isDirectory()) {
            for (File child : fileOrDirectory.listFiles()) {
                isOK = deleteRecursive(child) && isOK;
            }
        }

        return fileOrDirectory.delete() && isOK;
    }


    public static boolean move(
            File from,
            File to)
    {
        return copyRecursive(from, to) && deleteRecursive(from);
    }


    public static boolean copyRecursive(
            File from,
            File to)
    {
        if (from.isDirectory()) {
            if (!to.exists()) {
                if (!to.mkdir()) {
                    return false;
                }
            }

            for (String path : from.list()) {
                if (!copyRecursive(new File(from, path), new File(to, path))) {
                    return false;
                }
            }
        } else {

            try {
                InputStream in = new FileInputStream(from);
                OutputStream out = new FileOutputStream(to);
                byte[] buf = new byte[Constants.IO_BUFFER_SIZE];
                copyStream(in, out, buf, Constants.IO_BUFFER_SIZE);
                in.close();
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
        }
        return true;
    }


    public static void copyStream(
            InputStream is,
            OutputStream os,
            byte[] buffer,
            int bufferSize)
            throws IOException
    {
        int len;
        while ((len = is.read(buffer, 0, bufferSize)) > 0) {
            os.write(buffer, 0, len);
        }
    }


    public static void copy(
            InputStream is,
            OutputStream os)
            throws IOException
    {
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = is.read(buffer)) != -1) {
            os.write(buffer, 0, bytesRead);
        }
    }


    public static String getFileNameByUri(
            final Context context,
            Uri uri,
            String defaultName)
    {
        String fileName = defaultName;
        try {
            if (uri.getScheme().compareTo("content") == 0) {
                Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
                if (null != cursor) {
                    try {
                        if (cursor.moveToFirst()) {
                            int column_index =
                                    cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME);
                            fileName = cursor.getString(column_index);
                        }
                    } catch (Exception e) {
                        //Log.d(TAG, e.getLocalizedMessage());
                    } finally {
                        cursor.close();
                    }
                }
            } else if (uri.getScheme().compareTo("file") == 0) {
                fileName = uri.getLastPathSegment();
            } else {
                fileName = fileName + "_" + uri.getLastPathSegment();
            }
        } catch (Exception e) {
            //do nothing, only return default file name;
            Log.d(Constants.TAG, e.getLocalizedMessage());
        }
        return fileName;
    }


    public static String removeExtension(String filePath)
    {
        // These first few lines the same as Justin's
        File f = new File(filePath);

        // if it's a directory, don't remove the extension
        if (f.isDirectory()) {
            return filePath;
        }

        String name = f.getName();

        // Now we know it's a file - don't need to do any special hidden
        // checking or contains() checking because of:
        final int lastPeriodPos = name.lastIndexOf('.');
        if (lastPeriodPos <= 0) {
            // No period after first character - return name as it was passed in
            return filePath;
        } else {
            // Remove the last period and everything after it
            File renamed = new File(f.getParent(), name.substring(0, lastPeriodPos));
            return renamed.getPath();
        }
    }


    public static String getExtension(String filePath)
    {
        File f = new File(filePath);
        if (f.isDirectory()) {
            return "";
        }

        String name = f.getName();

        final int lastPeriodPos = name.lastIndexOf('.');
        if (lastPeriodPos <= 0) {
            return "";
        } else {
            return name.substring(lastPeriodPos + 1);
        }
    }


    public static long getDirectorySize(File dir)
    {
        long size = 0;
        if (dir == null || dir.listFiles() == null)
            return size;

        File[] files = dir.listFiles();

        for (File file : files) {
            if (file.isFile()) {
                size += file.length();
            } else {
                size += getDirectorySize(file);
            }
        }

        return size;
    }


    public static boolean isDirectoryWritable(File directory)
    {
        File toCreate = new File(directory, "hello");
        try {
            toCreate.createNewFile();
            return true;
        } catch (IOException e) {
            // It's expected we'll get a "Permission denied" exception.
        } finally {
            toCreate.delete();
        }
        return false;
    }

    public static String getPathSeparator() {
        return "/";
    }

    public static void unzipEntry(
            ZipInputStream zis,
            ZipEntry entry,
            byte[] buffer,
            File outputDir)
            throws IOException, RuntimeException
    {
        String entryName = entry.getName();
        int pos = entryName.indexOf('/');

        //for backward capability where the zip has root directory named "mapnik"
        if (pos != Constants.NOT_FOUND) {
            String folderName = entryName.substring(0, pos);
            if (!TextUtils.isDigitsOnly(folderName)) {
                entryName = entryName.substring(pos);
            }
        }

        if (entry.isDirectory()) {
            FileUtil.createDir(new File(outputDir, entryName));
            return;
        }

        //for prevent searching by media library
        entryName = entryName.replace(".png", TMSLayer.TILE_EXT);
        entryName = entryName.replace(".jpg", TMSLayer.TILE_EXT);
        entryName = entryName.replace(".jpeg", TMSLayer.TILE_EXT);

        if (entryName.toLowerCase().equals("mapnik.json"))
            entryName = "config.json";

        File outputFile = new File(outputDir, entryName);
        if (!outputFile.getParentFile().exists()) {
            FileUtil.createDir(outputFile.getParentFile());
        }
        FileOutputStream fout = new FileOutputStream(outputFile);
        FileUtil.copyStream(zis, fout, buffer, Constants.IO_BUFFER_SIZE);
        fout.close();
    }
} public interface GeoConstants
{

    /**
     * Mercator projection constants
     */
    double MERCATOR_MAX = 20037508.34;
    double WGS_LONG_MAX = 180;
    double WGS_LAT_MAX  = 90;

    /**
     * TMS type
     */
    int TMSTYPE_NORMAL = 1;
    int TMSTYPE_OSM    = 2;

    int DEFAULT_MAX_ZOOM = 25;
    int DEFAULT_CACHE_MAX_ZOOM = 18;
    int DEFAULT_MIN_ZOOM = 0;

    /**
     * geometry type
     */
    int GTPoint              = 1;
    int GTLineString         = 2;
    int GTPolygon            = 3;
    int GTMultiPoint         = 4;
    int GTMultiLineString    = 5;
    int GTMultiPolygon       = 6;
    int GTGeometryCollection = 7;
    int GTNone               = 100;
    int GTLinearRing         = 200;

    int GTPointCheck              = 1 << GTPoint;
    int GTLineStringCheck         = 1 << GTLineString;
    int GTPolygonCheck            = 1 << GTPolygon;
    int GTMultiPointCheck         = 1 << GTMultiPoint;
    int GTMultiLineStringCheck    = 1 << GTMultiLineString;
    int GTMultiPolygonCheck       = 1 << GTMultiPolygon;
    int GTGeometryCollectionCheck = 1 << GTGeometryCollection;
    int GTNoneCheck               = 1 << 10;
    int GTAnyCheck                =
            GTMultiPointCheck | GTPointCheck | GTLineStringCheck | GTMultiLineStringCheck |
                    GTPolygonCheck | GTMultiPolygonCheck | GTGeometryCollectionCheck;

    /**
     * geojson see http://geojson.org/geojson-spec.html
     */
    String GEOJSON_CRS_EPSG_4326           = "EPSG:4326";
    String GEOJSON_CRS_WGS84               = "urn:ogc:def:crs:OGC:1.3:CRS84";
    String GEOJSON_CRS_WEB_MERCATOR        = "EPSG:3857";
    String GEOJSON_CRS_EPSG_3857           = "urn:ogc:def:crs:EPSG::3857";
    String GEOJSON_TYPE                    = "type";
    String GEOJSON_ID                      = "id";
    String GEOJSON_FEATURE_ID              = "FEATURE_ID";
    String GEOJSON_CRS                     = "crs";
    String GEOJSON_NAME                    = "name";
    String GEOJSON_PROPERTIES              = "properties";
    String GEOJSON_ATTACHES                = "attaches";
    String GEOJSON_BBOX                    = "bbox";
    String GEOJSON_TYPE_FEATURES           = "features";
    String GEOJSON_GEOMETRY                = "geometry";
    String GEOJSON_GEOMETRIES              = "geometries";
    String GEOJSON_COORDINATES             = "coordinates";
    String GEOJSON_TYPE_Point              = "Point";
    String GEOJSON_TYPE_MultiPoint         = "MultiPoint";
    String GEOJSON_TYPE_LineString         = "LineString";
    String GEOJSON_TYPE_MultiLineString    = "MultiLineString";
    String GEOJSON_TYPE_Polygon            = "Polygon";
    String GEOJSON_TYPE_MultiPolygon       = "MultiPolygon";
    String GEOJSON_TYPE_GeometryCollection = "GeometryCollection";
    String GEOJSON_TYPE_Feature            = "Feature";
    String GEOJSON_TYPE_FeatureCollection  = "FeatureCollection";

    /**
     * field type
     */
    int FTInteger     = 0;
    int FTIntegerList = 1;
    int FTReal        = 2;
    int FTRealList    = 3;
    int FTString      = 4;
    int FTStringList  = 5;
    int FTBinary      = 8;
    int FTDateTime    = 10;
    int FTDate        = 11;
    int FTTime        = 12;

    /**
     * CRS
     */
    int CRS_WGS84        = 4326;
    int CRS_WEB_MERCATOR = 3857;

} public class GeoJSONUtil {

    /**
     * Check if provided name support and throw NGWException if not. Now support EPSG 3857 and 4326 only.
     * @param crsName Spatial reference name
     * @param context Context to get error string
     * @return true if provided string is WGS84 (4326) or false if Web Mercator (3857)/
     * @throws NGException
     */
    public static boolean checkCRSSupportAndWGS(String crsName, Context context) throws NGException {
        boolean isWGS84;
        switch (crsName) {
            case GeoConstants.GEOJSON_CRS_WGS84:  // WGS84
            case GeoConstants.GEOJSON_CRS_EPSG_4326:  // WGS84
                isWGS84 = true;
                break;
            case GeoConstants.GEOJSON_CRS_EPSG_3857:
            case GeoConstants.GEOJSON_CRS_WEB_MERCATOR:  //Web Mercator
                isWGS84 = false;
                break;
            default:
                throw new NGException(context.getString(R.string.error_crs_unsupported));
        }
        return isWGS84;
    }

    protected static Object parseNumber(String value) {
        try {
            if (value.contains(".") || value.contains(",")) {
                return Double.parseDouble(value);
            } else {
                return Long.parseLong(value);
            }
        }
        catch (NumberFormatException e) {
            e.printStackTrace();
            return null;
        }
    }

    protected static Object parseDateTime(Object value, int type) {
        SimpleDateFormat sdf = null;

        switch (type) {
            case GeoConstants.FTDate:
                sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
                break;
            case GeoConstants.FTTime:
                sdf = new SimpleDateFormat("HH:mm", Locale.getDefault());
                break;
            case GeoConstants.FTDateTime:
                sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm", Locale.getDefault());
                break;
        }

        if (sdf != null && value instanceof String)
            try {
                value = sdf.parse((String) value).getTime();
            } catch (ParseException e) {
                e.printStackTrace();
            }

        return value;
    }

    public static String formatDateTime(long millis, int type) {
        String result = millis + "";
        SimpleDateFormat sdf = null;

        switch (type) {
            case FTDate:
                sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
                break;
            case FTTime:
                sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
                break;
            case FTDateTime:
                sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
                break;
        }

        if (sdf != null)
            try {
                result = sdf.format(new Date(millis));
            } catch (Exception e) {
                e.printStackTrace();
            }

        return result;
    }

    public static void fillLayerFromGeoJSONStream(VectorLayer layer, InputStream in, int srs, IProgressor progressor) throws IOException, NGException {
        int streamSize = in.available();
        if(null != progressor){
            progressor.setIndeterminate(false);
            progressor.setMax(streamSize);
            progressor.setMessage(layer.getContext().getString(R.string.start_fill_layer) + " " + layer.getName());
        }

        SQLiteDatabase db = null;
        if(layer.getFields() != null && layer.getFields().isEmpty()){
            db = DatabaseContext.getDbForLayer(layer);
        }

        JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
        boolean isWGS84 = srs == GeoConstants.CRS_WGS84;
        long counter = 0;
        reader.beginObject();
        while (reader.hasNext()) {
            String name = reader.nextName();
            if(name.equals(GeoConstants.GEOJSON_TYPE_FEATURES)){
                reader.beginArray();
                while (reader.hasNext()) {
                    Feature feature = readGeoJSONFeature(reader, layer, isWGS84);
                    if (null != feature) {
                        if(layer.getFields() != null && !layer.getFields().isEmpty()){
                            if (feature.getGeometry() != null)
                                layer.create(feature.getGeometry().getType(), feature.getFields());

                            db = DatabaseContext.getDbForLayer(layer);
                        }

                        if(feature.getGeometry() != null) {
                            layer.createFeatureBatch(feature, db);
                            if(null != progressor){
                                if (progressor.isCanceled()) {
                                    layer.save();
                                    return;
                                }
                                progressor.setValue(streamSize - in.available());
                                progressor.setMessage(layer.getContext().getString(R.string.process_features) + ": " + counter++);
                            }
                        }
                    }
                }
                reader.endArray();
            }
            else {
                reader.skipValue();
            }
        }
        reader.endObject();
        reader.close();

        //if(null != db)
        //    db.close(); // return pragma to init

        layer.save();
    }

    // TODO refactor it and fillLayerFromGeoJsonStream
    public static void createLayerFromGeoJSONStream(VectorLayer layer, InputStream in, IProgressor progressor, boolean isWGS84) throws IOException, NGException {
        int streamSize = in.available();
        if(null != progressor){
            progressor.setIndeterminate(false);
            progressor.setMax(streamSize);
            progressor.setMessage(layer.getContext().getString(R.string.start_fill_layer) + " " + layer.getName());
        }

        SQLiteDatabase db = null;
        if(layer.getFields() != null && layer.getFields().isEmpty()){
            db = DatabaseContext.getDbForLayer(layer);
        }

        JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
        long counter = 0;
        reader.beginObject();
        while (reader.hasNext()) {
            String name = reader.nextName();
            if (name.equals(GeoConstants.GEOJSON_TYPE_FEATURES)) {
                reader.beginArray();
                while (reader.hasNext()) {
                    Feature feature = readGeoJSONFeature(reader, layer, isWGS84);
                    if (null != feature) {
                        if (layer.getFields() == null || layer.getFields().isEmpty()) {
                            if (feature.getGeometry() != null)
                                layer.create(feature.getGeometry().getType(), feature.getFields());

                            db = DatabaseContext.getDbForLayer(layer);
                        }

                        if (feature.getGeometry() != null) {
                            layer.createFeatureBatch(feature, db);
                            if(null != progressor){
                                if (progressor.isCanceled()) {
                                    layer.save();
                                    return;
                                }
                                progressor.setValue(streamSize - in.available());
                                progressor.setMessage(layer.getContext().getString(R.string.process_features) + ": " + counter++);
                            }
                        }
                    }
                }
                reader.endArray();
            } else {
                reader.skipValue();
            }
        }
        reader.endObject();
        reader.close();

        //if(null != db)
        //   db.close(); // return pragma to init
        layer.save();
    }

    private static Feature readGeoJSONFeature(JsonReader reader, VectorLayer layer, boolean isWGS84) throws IOException {
        Feature feature;
        if(layer.getFields() != null && !layer.getFields().isEmpty())
            feature = new Feature(Constants.NOT_FOUND, layer.getFields());
        else
            feature = new Feature();

        int crs = isWGS84 ? GeoConstants.CRS_WGS84 : GeoConstants.CRS_WEB_MERCATOR;
        reader.beginObject();
        while (reader.hasNext()) {
            String name = reader.nextName();
            if (name.equals(GeoConstants.GEOJSON_PROPERTIES)) {
                readGeoJSONFeatureProperties(reader, layer, feature);
            } else if (name.equals(GeoConstants.GEOJSON_GEOMETRY)) {
                GeoGeometry geometry = GeoGeometryFactory.fromJsonStream(reader, crs);
                if (null != geometry) {
                    if (isWGS84) {
                        geometry.setCRS(GeoConstants.CRS_WGS84);
                        geometry.project(GeoConstants.CRS_WEB_MERCATOR);
                    }

                    feature.setGeometry(geometry);
                }
            } else {
                reader.skipValue();
            }
        }
        reader.endObject();
        return feature;
    }


    private static int getOrCreateField(String name, int type, VectorLayer layer, Feature feature) {
        boolean isFieldsFilled = layer.getFields() != null && !layer.getFields().isEmpty();
        if (isFieldsFilled) {
            int fieldIndex = feature.getFieldValueIndex(name);
            if(Constants.NOT_FOUND == fieldIndex){
                Field field = new Field(type, name, name);
                layer.createField(field);
                feature.getFields().add(field);
            }
        }
        else {
            Field field = new Field(GeoConstants.FTString, name, name);
            feature.getFields().add(field);
        }

        return feature.getFieldValueIndex(name);
    }

    private static void readGeoJSONFeatureProperties(JsonReader reader, VectorLayer layer, Feature feature) throws IOException {
        reader.beginObject();
        while (reader.hasNext()) {
            String name = LayerUtil.normalizeFieldName(reader.nextName());
            if (!LayerUtil.isFieldNameValid(name)) {
                reader.skipValue();
            } else {
                JsonToken jsonToken = reader.peek();
                if (jsonToken == JsonToken.STRING) {
                    String value = reader.nextString();
                    int fieldIndex = getOrCreateField(name, GeoConstants.FTString, layer, feature);
                    if (fieldIndex < 0) {
                        continue;
                    }

                    int fieldType = feature.getFields().get(fieldIndex).getType();
                    if (fieldType == GeoConstants.FTDate || fieldType == GeoConstants.FTDateTime || fieldType == GeoConstants.FTTime) {
                        feature.setFieldValue(fieldIndex, parseDateTime(value, fieldType));
                    } else {
                        feature.setFieldValue(fieldIndex, value);
                    }
                }
                else if (jsonToken == JsonToken.BOOLEAN) {
                    boolean value = reader.nextBoolean();
                    int fieldIndex = getOrCreateField(name, GeoConstants.FTInteger, layer, feature);
                    if (fieldIndex < 0) {
                        continue;
                    }

                    feature.setFieldValue(name, value ? 1 : 0);
                } else if (jsonToken == JsonToken.NUMBER) {
                    String value = reader.nextString();
                    int fieldIndex;
                    Object number = parseNumber(value);
                    if (null != number) {
                        if (number instanceof Double)
                            fieldIndex = getOrCreateField(name, GeoConstants.FTReal, layer, feature);
                        else
                            fieldIndex = getOrCreateField(name, GeoConstants.FTInteger, layer, feature);

                        if (fieldIndex < 0) {
                            continue;
                        }

                        feature.setFieldValue(name, number);
                    }
                } else {
                    reader.skipValue();
                }
            }
        }
        reader.endObject();
    }

    public static boolean readGeoJSONCRS(InputStream is, Context context) throws IOException, NGException {
        JsonReader reader = new JsonReader(new InputStreamReader(is, "UTF-8"));
        boolean isWGS = true;
        reader.beginObject();
        while (reader.hasNext()) {
            String name = reader.nextName();
            if(name.equals(GeoConstants.GEOJSON_CRS)) {
                reader.beginObject();
                while (reader.hasNext()) {
                    name = reader.nextName();
                    if(name.equals(GeoConstants.GEOJSON_PROPERTIES)) {
                        reader.beginObject();
                        while (reader.hasNext()) {
                            String subname = reader.nextName();
                            if(subname.equals(GeoConstants.GEOJSON_NAME)){
                                String val = reader.nextString();
                                isWGS = checkCRSSupportAndWGS(val, context);
                            }
                            else {
                                reader.skipValue();
                            }
                        }
                        reader.endObject();
                    } else {
                        reader.skipValue();
                    }
                }
                reader.endObject();
            } else {
                reader.skipValue();
            }
        }
        reader.endObject();
        reader.close();
        return isWGS;
    }


    public static boolean isGeoJsonHasFeatures(File path) throws IOException {
        FileInputStream inputStream = new FileInputStream(path);
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        char[] buffer = new char[Constants.IO_BUFFER_SIZE];
        inputStreamReader.read(buffer, 0, Constants.IO_BUFFER_SIZE);
        inputStream.close();

        String testString = new String(buffer);

        return testString.contains("\"" + GeoConstants.GEOJSON_TYPE_FEATURES + "\"");
    }

} public class LayerUtil {
    public static boolean containsCaseInsensitive(
            String strToCompare,
            String[] list)
    {
        for (String str : list) {
            if (str.equalsIgnoreCase(strToCompare)) {
                return (true);
            }
        }
        return (false);
    }

    public static boolean isFieldNameValid(String fieldName){
        return !containsCaseInsensitive(fieldName, Constants.VECTOR_FORBIDDEN_FIELDS);
    }

    public static String normalizeFieldName(String fieldName) {
        char [] forbiddenChars = {':', '@', '#', '%', '^', '&', '*', '!', '$', '(', ')', '+', '-', '?', '=', '/', '\\', '"', '\'', '[', ']', ',', ' '};
        String result = fieldName;

        if (Character.isDigit(result.charAt(0)))
            result = "_" + result;

        for(char testChar : forbiddenChars)
            result = result.replace(testChar, '_');

        if(result.equals(Constants.FIELD_ID))
            return "_fixed_id";

        return result;
    }

    public static String normalizeLayerName(String layerName) {
        char [] forbiddenChars = {':', '<', '>', '*', '?', '/', '\\', '"', '|'};
        String result = layerName;

        if (result.charAt(layerName.length() - 1) == '.')
            result += "_";

        for (char testChar : forbiddenChars)
            result = result.replace(testChar, '_');

        return result;
    }

    public static int stringToType(String type)
    {
        switch (type) {
            case "STRING":
                return GeoConstants.FTString;
            case "INTEGER":
            case "BIGINT":
                return GeoConstants.FTInteger;
            case "REAL":
                return GeoConstants.FTReal;
            case "DATETIME":
                return GeoConstants.FTDateTime;
            case "DATE":
                return GeoConstants.FTDate;
            case "TIME":
                return GeoConstants.FTTime;
            default:
                return Constants.NOT_FOUND;
        }
    }

    public static String typeToString(int type) {
        switch (type) {
            case GeoConstants.FTString:
                return "STRING";
            case GeoConstants.FTInteger:
                return "INTEGER";
            case GeoConstants.FTReal:
                return "REAL";
            case GeoConstants.FTDateTime:
                return "DATETIME";
            case GeoConstants.FTDate:
                return "DATE";
            case GeoConstants.FTTime:
                return "TIME";
            default:
                return "";
        }
    }

    public static String typeToString(Context context, int type) {
        switch (type) {
            case GeoConstants.FTString:
                return context.getString(R.string.field_type_string);
            case GeoConstants.FTInteger:
                return context.getString(R.string.field_type_int);
            case GeoConstants.FTReal:
                return context.getString(R.string.field_type_real);
            case GeoConstants.FTDateTime:
                return context.getString(R.string.field_type_datetime);
            case GeoConstants.FTDate:
                return context.getString(R.string.field_type_date);
            case GeoConstants.FTTime:
                return context.getString(R.string.field_type_time);
            default:
                return "n/a";
        }
    }


    public static String getSelectionForIds(List<Long> ids)
    {
        if (null == ids) {
            return null;
        }

        StringBuilder sb = new StringBuilder(1024);

        for (Long id : ids) {
            if (sb.length() == 0) {
                sb.append(com.nextgis.maplib.util.Constants.FIELD_ID);
                sb.append(" IN (");
            } else {
                sb.append(",");
            }
            sb.append(id);
        }

        if (sb.length() > 0) {
            sb.append(")");
        }

        return sb.toString();
    }
} public class MapUtil {
    private static final long MAX_INTERNAL_CACHE_SIZE = 1048576; // 1MB
    private static final long MAX_EXTERNAL_CACHE_SIZE = 5242880; // 5MB

    public static File prepareTempDir(Context context, String path, Boolean preserveCache) {
        boolean clearCached;
        File temp = context.getExternalCacheDir();

        if (temp == null) {
            temp = context.getCacheDir();
            clearCached = FileUtil.getDirectorySize(temp) > MAX_INTERNAL_CACHE_SIZE;
        } else {
            clearCached = FileUtil.getDirectorySize(temp) > MAX_EXTERNAL_CACHE_SIZE;
        }

        if (clearCached && !preserveCache)
            FileUtil.deleteRecursive(temp);

        if (path != null)
            temp = new File(temp, path);

        try {
            FileUtil.createDir(temp);
        } catch (RuntimeException e) {
            e.printStackTrace();
        }

        return temp;
    }

    public static double lg(double x)
    {
        return Math.log(x) / Math.log(2.0);
    }

    public static float getZoomForScaleFactor(double scale, float currentZoom)
    {
        float zoom = currentZoom;
        if (scale > 1) {
            zoom = (float) (currentZoom + MapUtil.lg(scale));
        } else if (scale < 1) {
            zoom = (float) (currentZoom - MapUtil.lg(1 / scale));
        }
        return zoom;
    }

    public static double getPixelSize(int zoom){
        int tilesInMapOneDimension = 1 << zoom;
        long sizeOneDimensionPixels = tilesInMapOneDimension * Constants.DEFAULT_TILE_SIZE;
        return GeoConstants.MERCATOR_MAX * 2 / sizeOneDimensionPixels;
    }

    public static long getTileCount(AsyncTask task, GeoEnvelope bounds, double zoom, int tmsType) {
        int decimalZoom = (int) zoom;
        int tilesInMapOneDimension = 1 << decimalZoom;

        double halfTilesInMapOneDimension = tilesInMapOneDimension * 0.5;
        double tilesSizeOneDimension = GeoConstants.MERCATOR_MAX / halfTilesInMapOneDimension;

        int begX = (int) Math.floor(bounds.getMinX() / tilesSizeOneDimension + halfTilesInMapOneDimension);
        int begY = (int) Math.floor(bounds.getMinY() / tilesSizeOneDimension + halfTilesInMapOneDimension);
        int endX = (int) Math.ceil(bounds.getMaxX() / tilesSizeOneDimension + halfTilesInMapOneDimension);
        int endY = (int) Math.ceil(bounds.getMaxY() / tilesSizeOneDimension + halfTilesInMapOneDimension);

        if (begY == endY)
            endY++;

        if (begX == endX)
            endX++;

        if (begY < 0)
            begY = 0;

        if (endY > tilesInMapOneDimension)
            endY = tilesInMapOneDimension;

        // normal fill from left bottom corner
        long total = 0;
        int realY;
        for (int x = begX; x < endX; x++) {
            if (task != null && task.isCancelled())
                return total;

            for (int y = begY; y < endY; y++) {
                realY = y;
                if (tmsType == GeoConstants.TMSTYPE_OSM)
                    realY = tilesInMapOneDimension - y - 1;

                if (realY < 0 || realY >= tilesInMapOneDimension)
                    continue;

                total++;
            }
        }

        return total;
    }

    public static List<TileItem> getTileItems(GeoEnvelope bounds, double zoom, int tmsType) {
        int decimalZoom = (int) zoom;
        int tilesInMapOneDimension = 1 << decimalZoom;
        double halfTilesInMapOneDimension = tilesInMapOneDimension * 0.5;

        double tilesSizeOneDimension = GeoConstants.MERCATOR_MAX / halfTilesInMapOneDimension;
        final List<TileItem> result = new LinkedList<>();

        int begX = (int) Math.floor(bounds.getMinX() / tilesSizeOneDimension + halfTilesInMapOneDimension);
        int begY = (int) Math.floor(bounds.getMinY() / tilesSizeOneDimension + halfTilesInMapOneDimension);
        int endX = (int) Math.ceil(bounds.getMaxX() / tilesSizeOneDimension + halfTilesInMapOneDimension);
        int endY = (int) Math.ceil(bounds.getMaxY() / tilesSizeOneDimension + halfTilesInMapOneDimension);

        if(begY == endY)
            endY++;
        if(begX == endX)
            endX++;

        if (begY < 0) {
            begY = 0;
        }
        if (endY > tilesInMapOneDimension) {
            endY = tilesInMapOneDimension;
        }
        /* this block unlimited X scroll of the map
        if (begX < 0) {
            begX = 0;
        }
        if (endX > tilesInMapOneDimension) {
            endX = tilesInMapOneDimension;
        }
        */

        // normal fill from left bottom corner
        int realX, realY;
        double fullBoundsMinX = -GeoConstants.MERCATOR_MAX;
        double fullBoundsMinY = -GeoConstants.MERCATOR_MAX;
        for (int x = begX; x < endX; x++) {
            if (Thread.currentThread().isInterrupted()) {
                break;
            }
            for (int y = begY; y < endY; y++) {
                if (Thread.currentThread().isInterrupted()) {
                    break;
                }
                realX = x;
                if (realX < 0) {
                    realX += tilesInMapOneDimension;
                } else if (realX >= tilesInMapOneDimension) {
                    realX -= tilesInMapOneDimension;
                }

                realY = y;
                if (tmsType == GeoConstants.TMSTYPE_OSM) {
                    realY = tilesInMapOneDimension - y - 1;
                }

                if (realY < 0 || realY >= tilesInMapOneDimension) {
                    continue;
                }

                double minX = fullBoundsMinX + x * tilesSizeOneDimension;
                double minY = fullBoundsMinY + y * tilesSizeOneDimension;
                final GeoEnvelope env = new GeoEnvelope(
                        minX,
                        minX + tilesSizeOneDimension,
                        minY,
                        minY + tilesSizeOneDimension);
                TileItem item = new TileItem(realX, realY, decimalZoom, env);
                result.add(item);

                if(result.size() > Constants.MAX_TILES_COUNT) // some limits for tiles array size
                    return result;
            }
        }

        return result;
    }

    public static boolean isZippedGeoJSON(Context context, AtomicReference<Uri> uri) {
        try {
            InputStream inputStream = context.getContentResolver().openInputStream(uri.get());
            if (inputStream == null)
                return false;

            byte[] buffer = new byte[Constants.IO_BUFFER_SIZE];
            ZipInputStream zis = new ZipInputStream(inputStream);
            ZipEntry ze;

            while ((ze = zis.getNextEntry()) != null) {
                if (ze.getName().toLowerCase().endsWith(".geojson")) {
                    File temp = prepareTempDir(context, null, false);
                    FileUtil.unzipEntry(zis, ze, buffer, temp);
                    temp = new File(temp, ze.getName());
                    uri.set(Uri.fromFile(temp));
                    zis.closeEntry();
                    return true;
                }
            }
        } catch (IOException | RuntimeException e) {
            e.printStackTrace();
        }

        return false;
    }

    public static boolean isParsable(String string) {
        try {
            //noinspection ResultOfMethodCallIgnored
            Long.parseLong(string);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    public static String makePlaceholders(int size) {
        if (size <= 0)
            return "";

        StringBuilder sb = new StringBuilder(size * 2 - 1);
        sb.append("?");

        for (int i = 1; i < size; i++) {
            sb.append(",?");
        }

        return sb.toString();
    }

    public static double getScaleInCm(Activity activity, int zoom) {
        DisplayMetrics dm = new DisplayMetrics();
        activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
        double ppcm = dm.density * dm.xdpi / 2.54; // get px per cm on display
        ppcm = 256 / ppcm; // get cm on display per 256 px tile
        ppcm = 40075160 / ppcm; // get real m per cm on display
        return ppcm / Math.pow(2, zoom); // take zoom in count
    }


    public static boolean isGeometryIntersects(Context context, GeoGeometry geometry) {
        if (geometry instanceof GeoPolygon) {
            if (((GeoPolygon) geometry).intersects()) {
                Toast.makeText(context, R.string.self_intersection, Toast.LENGTH_SHORT).show();
                return true;
            }

            if (!((GeoPolygon) geometry).isHolesInside()) {
                Toast.makeText(context, R.string.ring_outside, Toast.LENGTH_SHORT).show();
                return true;
            }

            if (((GeoPolygon) geometry).isHolesIntersect()) {
                Toast.makeText(context, R.string.rings_intersection, Toast.LENGTH_SHORT).show();
                return true;
            }
        }

        if (geometry instanceof GeoMultiPolygon) {
            if (((GeoMultiPolygon) geometry).isSelfIntersects()) {
                Toast.makeText(context, R.string.self_intersection, Toast.LENGTH_SHORT).show();
                return true;
            }

            if (!((GeoMultiPolygon) geometry).isHolesInside()) {
                Toast.makeText(context, R.string.ring_outside, Toast.LENGTH_SHORT).show();
                return true;
            }

            if (((GeoMultiPolygon) geometry).isHolesIntersect()) {
                Toast.makeText(context, R.string.rings_intersection, Toast.LENGTH_SHORT).show();
                return true;
            }
        }

        return false;
    }
} public class NGException extends Exception {
    public NGException(String message) {
        super(message);
    }

    public NGException(String message, Throwable throwable) {
        super(message, throwable);
    }
} public class NGWUtil
{
    public static String NGWKEY_ID              = "id";
    public static String NGWKEY_GEOM            = "geom";
    public static String NGWKEY_FIELDS          = "fields";
    public static String NGWKEY_SRS             = "srs";
    public static String NGWKEY_EXTENSIONS      = "extensions";
    public static String NGWKEY_NAME            = "name";
    public static String NGWKEY_MIME            = "mime_type";
    public static String NGWKEY_DESCRIPTION     = "description";
    public static String NGWKEY_DISPLAY_NAME    = JSON_DISPLAY_NAME;
    public static String NGWKEY_YEAR            = "year";
    public static String NGWKEY_MONTH           = "month";
    public static String NGWKEY_DAY             = "day";
    public static String NGWKEY_HOUR            = "hour";
    public static String NGWKEY_MINUTE          = "minute";
    public static String NGWKEY_SECOND          = "second";
    public static String NGWKEY_FEATURE_COUNT   = "total_count";
    public static String NGWKEY_KEYNAME         = "keyname";
    public static String NGWKEY_PASSWORD        = "password";
    public static String NGWKEY_CLS             = "cls";
    public static String NGWKEY_VECTOR_LAYER    = "vector_layer";
    public static String NGWKEY_RESOURCE_GROUP  = "resource_group";
    public static String NGWKEY_PARENT          = "parent";
    public static String NGWKEY_GEOMETRY_TYPE   = "geometry_type";
    public static String NGWKEY_DATATYPE        = "datatype";
    public static String NGWKEY_LOOKUP_TABLE    = "lookup_table";
    public static String NGWKEY_RESMETA         = "resmeta";
    public static String NGWKEY_ITEMS           = "items";
    public static String NGUA = "";
    public static String NGID = "";
    public static String UUID = "";

    /**
     * NGW API Functions
     */

    public static String getConnectionCookie(
            AtomicReference<String> reference,
            String login,
            String password)
            throws IOException
    {
        String sUrl = reference.get();
        if (!sUrl.startsWith("http")) {
            sUrl = "http://" + sUrl;
            reference.set(sUrl);
        }

        sUrl += "/login";
        String sPayload = "login=" + login + "&password=" + password;
        final HttpURLConnection conn = NetworkUtil.getHttpConnection("POST", sUrl, null, null);
        if (null == conn) {
            Log.d(TAG, "Error get connection object: " + sUrl);
            return null;
        }
        conn.setInstanceFollowRedirects(false);
        conn.setDefaultUseCaches(false);
        conn.setDoOutput(true);
        conn.connect();

        OutputStream os = conn.getOutputStream();
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
        writer.write(sPayload);

        writer.flush();
        writer.close();
        os.close();

        int responseCode = conn.getResponseCode();
        if (!(responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == HttpURLConnection.HTTP_MOVED_PERM)) {
            Log.d(TAG, "Problem execute post: " + sUrl + " HTTP response: " + responseCode);
            return null;
        }

        String headerName;
        for (int i = 1; (headerName = conn.getHeaderFieldKey(i)) != null; i++) {
            if (headerName.equals("Set-Cookie")) {
                return conn.getHeaderField(i);
            }
        }

        if (!sUrl.startsWith("https")) {
            sUrl = sUrl.replace("http", "https").replace("/login", "");
            reference.set(sUrl);
        }

        return getConnectionCookie(reference, login, password);
    }

    public static String appendix() {
        return "?source=" + NGUA + "&ngid=" + NGID + "&deviceid=" + UUID;
    }

    public static String getServerUrl(String server) {
        if (!server.startsWith("http")) {
            server = "http://" + server;
        }
        return server;//.replace("http://", "https://");
    }

    public static String getFileUploadUrl(String server)
    {
        return getServerUrl(server) + "/api/component/file_upload/upload" + appendix();
    }


    public static String getNgwVersionUrl(String server)
    {
        return getServerUrl(server) + "/api/component/pyramid/pkg_version";
    }


    public static String getNgwUrlResolverUrl(String uuid)
    {
        return "https://groundcontrol.nimbo.nextgis.net/api/instance/" + uuid + "/url";
    }


    public static String getRealNgwUrlFromUuid(String url) throws IOException {
        HttpResponse response = NetworkUtil.get(url, null, null, false);
        if (response.isOk()) {
            return response.mResponseBody;
        }
        return null;
    }


    public static Pair<Integer, Integer> getNgwVersion(Context context, String account) {
        Pair<Integer, Integer> ver = new Pair<>(-1, -1);
        try {
            AccountUtil.AccountData accountData = AccountUtil.getAccountData(context, account);
            ver = NGWUtil.getNgwVersion(accountData.url, accountData.login, accountData.password);
        } catch (IOException | JSONException | NumberFormatException | IllegalStateException ignored) { }

        return ver;
    }


    public static Pair<Integer, Integer> getNgwVersion(
            String url,
            String login,
            String password)
            throws IOException, JSONException, NumberFormatException
    {
        HttpResponse response =
                NetworkUtil.get(NGWUtil.getNgwVersionUrl(url), login, password, false);
        if (response.isOk()) {
            JSONObject verJSONObject = new JSONObject(response.getResponseBody());
            String fullVer = verJSONObject.getString("nextgisweb");
            String[] verParts = fullVer.split("\\.");

            if (verParts.length >= 2) {
                try {
                    Integer major = Integer.parseInt(verParts[0]);
                    Integer minor = Integer.parseInt(verParts[1]);
                    return new Pair<>(major, minor);
                } catch (NumberFormatException ignored) { }
            }

            Log.d(TAG, "BAD format of the NGW version, must start with 'major.minor', obtained: " + fullVer);
        }

        return null;
    }


    /**
     * GeoJSON URL. Get data as GeoJSON
     *
     * @param server
     *         URL to NextGIS Web server
     * @param remoteId
     *         Vector layer resource id
     *
     * @return URL
     */
    public static String getGeoJSONUrl(
            String server,
            long remoteId)
    {
        return getResourceUrl(server, remoteId) + "/geojson";
    }


    /**
     * TMS URL for raster layer
     *
     * @param server
     *         URL
     * @param styleIds
     *         Raster style ids
     *
     * @return URL to TMS for TMSLayer
     */
    public static String getTMSUrl(
            String server,
            Long[] styleIds)
    {
        String ids = "";
        if (styleIds != null && styleIds.length > 0) {
            ids += styleIds[0];
            for (int i = 1; i < styleIds.length; i++) {
                ids += "," + styleIds[i];
            }
        }

        // old url return server + "/resource/" + styleId + "/tms?z={z}&x={x}&y={y}";
        return getServerUrl(server) + "/api/component/render/tile?x={x}&y={y}&z={z}&resource=" + ids;
    }


    /**
     * Resource URL
     *
     * @param server
     *         URL
     * @param remoteId
     *         resource id
     *
     * @return URL to resource
     */
    public static String getResourceUrl(
            String server,
            long remoteId)
    {
        return getBaseUrl(server) + remoteId;
    }


    /**
     * The resource base url
     *
     * @param server
     *         URL
     *
     * @return URL to base resource
     */
    public static String getBaseUrl(String server) {
        return getServerUrl(server) + "/api/resource/";
    }


    /**
     * The form url for vector layer
     *
     * @param server
     *         URL
     * @param remoteId
     *         resource id
     *
     * @return URL to resource
     */
    public static String getFormUrl(String server, long remoteId) {
        return getResourceUrl(server, remoteId) + "/ngfp";
    }


    /**
     * The resource's children url
     *
     * @param server
     *         URL
     *
     * @return URL to base resource
     */
    public static String getResourceChildrenUrl(String server, long resourceId) {
        return getBaseUrl(server) + "?parent=" + resourceId;
    }


    /**
     * Get one row from vector layer
     *
     * @param server
     *         URL
     * @param remoteId
     *         resource id
     * @param featureId
     *         row id
     *
     * @return URL to row
     */
    public static String getFeatureUrl(
            String server,
            long remoteId,
            long featureId)
    {
        return getFeaturesUrl(server, remoteId) + featureId + appendix();
    }


    /**
     * Get the url to JSONArray of features  (NOT GeoJSON!)
     *
     * @param server
     *         URL
     * @param remoteId
     *         vector layer id
     *
     * @return URL
     */
    public static String getFeaturesUrl(
            String server,
            long remoteId)
    {
        return getResourceUrl(server, remoteId) + "/feature/";
    }


    public static String getFeaturesUrl(
            String server,
            long remoteId,
            String where)
    {
        if (TextUtils.isEmpty(where))
            return getFeaturesUrl(server, remoteId);

        return getFeaturesUrl(server, remoteId) + "?" + where;
    }


    public static String getExtent(String server, long remoteId) {
        return getResourceUrl(server, remoteId) + "/extent";
    }


    public static String getTrackedFeaturesUrl(
            String server,
            long remoteId,
            long startDate)
    {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault());
        String start = sdf.format(new Date(startDate));
        return getServerUrl(server) + "/api/vector_layer/" + remoteId + "/diff/?ts_start=" + start;
    }


    public static String getFeatureAttachmentUrl(
            String server,
            long remoteId,
            long featureId)
    {
        return getFeaturesUrl(server, remoteId) + featureId + "/attachment/";
    }


    public static List<Field> getFieldsFromJson(JSONArray fieldsJSONArray)
            throws JSONException
    {
        List<Field> fields = new LinkedList<>();
        for (int i = 0; i < fieldsJSONArray.length(); i++) {
            JSONObject fieldJSONObject = fieldsJSONArray.getJSONObject(i);
            String type = fieldJSONObject.getString("datatype");
            String alias = fieldJSONObject.getString("display_name");
            String name = fieldJSONObject.getString("keyname");

            int nType = LayerUtil.stringToType(type);
            if (Constants.NOT_FOUND != nType) {
                fields.add(new Field(nType, name, alias));
            }
        }
        return fields;
    }


    public static Feature readNGWFeature(
            JsonReader reader,
            List<Field> fields,
            int nSRS)
            throws IOException, IllegalStateException, NumberFormatException, OutOfMemoryError
    {
        final Feature feature = new Feature(Constants.NOT_FOUND, fields);

        reader.beginObject();
        while (reader.hasNext()) {
            String name = reader.nextName();
            if (name.equals(NGWUtil.NGWKEY_ID)) {
                feature.setId(reader.nextLong());
            } else if (name.equals(NGWUtil.NGWKEY_GEOM)) {
                String wkt = reader.nextString();
                GeoGeometry geom = GeoGeometryFactory.fromWKT(wkt, nSRS);
                geom.setCRS(nSRS);
                if (nSRS != GeoConstants.CRS_WEB_MERCATOR) {
                    geom.project(GeoConstants.CRS_WEB_MERCATOR);
                }
                feature.setGeometry(geom);
            } else if (name.equals(NGWUtil.NGWKEY_FIELDS)) {
                readNGWFeatureFields(feature, reader, fields);
            } else if (name.equals(NGWUtil.NGWKEY_EXTENSIONS)) {
                if (reader.peek() != JsonToken.NULL) {
                    readNGWFeatureAttachments(feature, reader);
                }
            } else {
                reader.skipValue();
            }
        }
        reader.endObject();
        return feature;
    }


    public static void readNGWFeatureFields(Feature feature, JsonReader reader, List<Field> fields)
            throws IOException, IllegalStateException, NumberFormatException
    {
        reader.beginObject();
        while (reader.hasNext()) {
            String name = reader.nextName();
            if (reader.peek() == JsonToken.NULL) {
                reader.skipValue();
            } else {
                boolean bAdded = false;
                for (Field field : fields) {
                    if (field.getName().equals(name) || field.getName().equals(LayerUtil.normalizeFieldName(name))) {
                        switch (field.getType()) {
                            case GeoConstants.FTReal:
                                feature.setFieldValue(field.getName(), reader.nextDouble());
                                bAdded = true;
                                break;
                            case GeoConstants.FTInteger:
                                feature.setFieldValue(field.getName(), reader.nextInt());
                                bAdded = true;
                                break;
                            case GeoConstants.FTString:
                                feature.setFieldValue(field.getName(), reader.nextString());
                                bAdded = true;
                                break;
                            case GeoConstants.FTDate:
                            case GeoConstants.FTTime:
                            case GeoConstants.FTDateTime:
                                readNGWDate(feature, reader, field.getName());
                                bAdded = true;
                                break;
                            default:
                                break;
                        }
                        break;
                    }
                }
                if (!bAdded) {
                    reader.skipValue();
                }
            }
        }
        reader.endObject();
    }


    private static void readNGWDate(Feature feature, JsonReader reader, String fieldName)
            throws IOException
    {
        reader.beginObject();
        int nYear = 1900;
        int nMonth = 1;
        int nDay = 1;
        int nHour = 0;
        int nMinute = 0;
        int nSecond = 0;
        while (reader.hasNext()) {
            String name = reader.nextName();
            if (name.equals(NGWUtil.NGWKEY_YEAR)) {
                nYear = reader.nextInt();
            } else if (name.equals(NGWUtil.NGWKEY_MONTH)) {
                nMonth = reader.nextInt();
            } else if (name.equals(NGWUtil.NGWKEY_DAY)) {
                nDay = reader.nextInt();
            } else if (name.equals(NGWUtil.NGWKEY_HOUR)) {
                nHour = reader.nextInt();
            } else if (name.equals(NGWUtil.NGWKEY_MINUTE)) {
                nMinute = reader.nextInt();
            } else if (name.equals(NGWUtil.NGWKEY_SECOND)) {
                nSecond = reader.nextInt();
            } else {
                reader.skipValue();
            }
        }

        TimeZone timeZone = TimeZone.getDefault();
        timeZone.setRawOffset(0); // set to UTC
        Calendar calendar = new GregorianCalendar(timeZone);
        calendar.set(nYear, nMonth - 1, nDay, nHour, nMinute, nSecond);
        calendar.set(Calendar.MILLISECOND, 0); // we must to reset millis
        feature.setFieldValue(fieldName, calendar.getTimeInMillis());

        reader.endObject();
    }


    private static void readNGWFeatureAttachments(Feature feature, JsonReader reader)
            throws IOException
    {
        //add extensions
        reader.beginObject();
        while (reader.hasNext()) {
            String name = reader.nextName();
            if (name.equals("attachment") && reader.peek() != JsonToken.NULL) {
                reader.beginArray();
                while (reader.hasNext()) {
                    readNGWFeatureAttachment(feature, reader);
                }
                reader.endArray();
            } else {
                reader.skipValue();
            }
        }

        reader.endObject();
    }


    private static void readNGWFeatureAttachment(Feature feature, JsonReader reader)
            throws IOException
    {
        reader.beginObject();
        String attachId = "";
        String name = "";
        String mime = "";
        String descriptionText = "";
        while (reader.hasNext()) {
            String keyName = reader.nextName();
            if (reader.peek() == JsonToken.NULL) {
                reader.skipValue();
                continue;
            }

            if (keyName.equals(NGWUtil.NGWKEY_ID)) {
                attachId += reader.nextLong();
            } else if (keyName.equals(NGWUtil.NGWKEY_NAME)) {
                name += reader.nextString();
            } else if (keyName.equals(NGWUtil.NGWKEY_MIME)) {
                mime += reader.nextString();
            } else if (keyName.equals(NGWUtil.NGWKEY_DESCRIPTION)) {
                descriptionText += reader.nextString();
            } else {
                reader.skipValue();
            }
        }
        AttachItem item = new AttachItem(attachId, name, mime, descriptionText);
        feature.addAttachment(item);

        reader.endObject();
    }


    public static List<Feature> jsonToFeatures(
            JSONArray featuresJSONArray,
            List<Field> fields,
            int nSRS,
            IProgressor progressor)
            throws JSONException
    {
        List<Feature> features = new LinkedList<>();
        if (null != progressor) {
            progressor.setMax(featuresJSONArray.length());
            progressor.setIndeterminate(false);
        }

        for (int i = 0; i < featuresJSONArray.length(); i++) {
            JSONObject featureJSONObject = featuresJSONArray.getJSONObject(i);

            if (null != progressor) {
                progressor.setValue(i);
            }

            long id = featureJSONObject.getLong(NGWUtil.NGWKEY_ID);
            String wkt = featureJSONObject.getString(NGWUtil.NGWKEY_GEOM);
            JSONObject fieldsJSONObject = featureJSONObject.getJSONObject(NGWUtil.NGWKEY_FIELDS);
            Feature feature = new Feature(id, fields);
            GeoGeometry geom = GeoGeometryFactory.fromWKT(wkt, nSRS);
            if (null == geom)
                continue;

            geom.setCRS(nSRS);
            if (nSRS != GeoConstants.CRS_WEB_MERCATOR)
                geom.project(GeoConstants.CRS_WEB_MERCATOR);
            if (!geom.isValid())
                continue;

            feature.setGeometry(geom);

            for (Field field : fields) {
                if (field.getType() == GeoConstants.FTDateTime ||
                        field.getType() == GeoConstants.FTDate ||
                        field.getType() == GeoConstants.FTTime) {
                    if (!fieldsJSONObject.isNull(field.getName())) {
                        JSONObject dateJson = fieldsJSONObject.getJSONObject(field.getName());
                        int nYear = 1900;
                        int nMonth = 1;
                        int nDay = 1;
                        int nHour = 0;
                        int nMinute = 0;
                        int nSec = 0;
                        if (dateJson.has(NGWKEY_YEAR)) {
                            nYear = dateJson.getInt(NGWKEY_YEAR);
                        }
                        if (dateJson.has(NGWKEY_MONTH)) {
                            nMonth = dateJson.getInt(NGWKEY_MONTH);
                        }
                        if (dateJson.has(NGWKEY_DAY)) {
                            nDay = dateJson.getInt(NGWKEY_DAY);
                        }
                        if (dateJson.has(NGWKEY_HOUR)) {
                            nHour = dateJson.getInt(NGWKEY_HOUR);
                        }
                        if (dateJson.has(NGWKEY_MINUTE)) {
                            nMinute = dateJson.getInt(NGWKEY_MINUTE);
                        }
                        if (dateJson.has(NGWKEY_SECOND)) {
                            nSec = dateJson.getInt(NGWKEY_SECOND);
                        }

                        TimeZone timeZone = TimeZone.getDefault();
                        timeZone.setRawOffset(0); // set to UTC
                        Calendar calendar = new GregorianCalendar(timeZone);
                        calendar.set(nYear, nMonth - 1, nDay, nHour, nMinute, nSec);
                        calendar.set(Calendar.MILLISECOND, 0); // we must to reset millis
                        feature.setFieldValue(field.getName(), calendar.getTimeInMillis());
                    }
                } else {
                    if (!fieldsJSONObject.isNull(field.getName())) {
                        feature.setFieldValue(
                                field.getName(), fieldsJSONObject.get(field.getName()));
                    }
                }
            }

            //add extensions
            if (featureJSONObject.has("extensions")) {
                JSONObject ext = featureJSONObject.getJSONObject("extensions");
                //get attachment & description
                if (!ext.isNull("attachment")) {
                    JSONArray attachment = ext.getJSONArray("attachment");
                    for (int j = 0; j < attachment.length(); j++) {
                        JSONObject jsonAttachmentDetails = attachment.getJSONObject(j);
                        String attachId = "" + jsonAttachmentDetails.getLong(Constants.JSON_ID_KEY);
                        String name = jsonAttachmentDetails.getString(Constants.JSON_NAME_KEY);
                        String mime = jsonAttachmentDetails.getString("mime_type");
                        String descriptionText = jsonAttachmentDetails.getString("description");
                        AttachItem item = new AttachItem(attachId, name, mime, descriptionText);
                        feature.addAttachment(item);
                    }
                }
            }

            features.add(feature);
        }
        return features;
    }


    public static boolean signUp(
            String server,
            String login,
            String password,
            String displayName,
            String description)
    {
        server += server.endsWith("/") ? "" : "/";
        server += "api/component/auth/register";
        server = getServerUrl(server);

        JSONObject payload = new JSONObject();
        try {
            payload.put(NGWKEY_KEYNAME, login);
            payload.put(NGWKEY_PASSWORD, password);
            payload.put(NGWKEY_DISPLAY_NAME, displayName == null ? login : displayName);
            payload.put(NGWKEY_DESCRIPTION, description);

            HttpResponse response =
                    NetworkUtil.post(server, payload.toString(), null, null, false);
            return response.isOk() && new JSONObject(response.getResponseBody()).has(JSON_ID_KEY);
        } catch (JSONException | IOException e) {
            e.printStackTrace();
        }

        return false;
    }

    public static boolean getResourceByKey(Context context, INGWResource resource, Map<String, Resource> keys) {
        if (resource instanceof Connection) {
            Connection connection = (Connection) resource;
            connection.loadChildren();
        } else if (resource instanceof ResourceGroup) {
            ResourceGroup resourceGroup = (ResourceGroup) resource;
            resourceGroup.loadChildren();
        }

        for (int i = 0; i < resource.getChildrenCount(); ++i) {
            INGWResource childResource = resource.getChild(i);

            if (keys.containsKey(childResource.getKey()) && childResource instanceof Resource) {
                Resource ngwResource = (Resource) childResource;
                keys.put(ngwResource.getKey(), ngwResource);
            }

            boolean bIsFill = true;
            for (Map.Entry<String, Resource> entry : keys.entrySet()) {
                if (entry.getValue() == null) {
                    bIsFill = false;
                    break;
                }
            }

            if (bIsFill) {
                return true;
            }

            if (getResourceByKey(context, childResource, keys)) {
                return true;
            }
        }

        boolean bIsFill = true;

        for (Map.Entry<String, Resource> entry : keys.entrySet()) {
            if (entry.getValue() == null) {
                bIsFill = false;
                break;
            }
        }

        return bIsFill;
    }

    public static HttpResponse createNewResource(
            Context context,
            Connection connection,
            JSONObject json)
    {
        HttpResponse result;
        try {
            AccountUtil.AccountData accountData =
                    AccountUtil.getAccountData(context, connection.getName());
            result = NetworkUtil.post(getBaseUrl(accountData.url), json.toString(),
                    accountData.login, accountData.password, false);
        } catch (IOException e) {
            e.printStackTrace();
            result = new HttpResponse(500);
        } catch (IllegalStateException e) {
            e.printStackTrace();
            result = new HttpResponse(401);
        }

        return result;
    }

    public static HttpResponse createNewGroup(
            Context context,
            Connection connection,
            long parentId,
            String name,
            String keyName)
    {
        JSONObject payload = new JSONObject();
        try {
            JSONObject resource = new JSONObject();
            resource.put(NGWKEY_CLS, NGWKEY_RESOURCE_GROUP);
            JSONObject id = new JSONObject();
            id.put(JSON_ID_KEY, parentId);
            resource.put(NGWKEY_PARENT, id);
            resource.put(NGWKEY_DISPLAY_NAME, name);
            if (keyName != null) {
                resource.put(NGWKEY_KEYNAME, keyName);
            }
            payload.put(JSON_RESOURCE_KEY, resource);
        } catch (JSONException e) {
            e.printStackTrace();
            return new HttpResponse(500);
        }

        return createNewResource(context, connection, payload);
    }

    public static HttpResponse createNewLayer(
            Connection connection,
            VectorLayer layer,
            long parentId,
            String keyName)
    {
        JSONObject payload = new JSONObject();
        try {
            JSONObject resource = new JSONObject();
            resource.put(NGWKEY_CLS, NGWKEY_VECTOR_LAYER);
            JSONObject id = new JSONObject();
            id.put(JSON_ID_KEY, parentId);
            resource.put(NGWKEY_PARENT, id);
            resource.put(JSON_DISPLAY_NAME, layer.getName());
            if (keyName != null) {
                resource.put(NGWKEY_KEYNAME, keyName);
            }
            payload.put(JSON_RESOURCE_KEY, resource);

            JSONObject vectorLayer = new JSONObject();
            JSONObject srs = new JSONObject();
            srs.put(JSON_ID_KEY, GeoConstants.CRS_WEB_MERCATOR);
            vectorLayer.put(NGWKEY_SRS, srs);
            vectorLayer.put(
                    NGWKEY_GEOMETRY_TYPE, GeoGeometryFactory.typeToString(layer.getGeometryType()));
            JSONArray fields = new JSONArray();
            for (Field field : layer.getFields()) {
                JSONObject current = new JSONObject();
                current.put(NGWKEY_KEYNAME, field.getName());
                current.put(NGWKEY_DISPLAY_NAME, field.getAlias());
                current.put(NGWKEY_DATATYPE, LayerUtil.typeToString(field.getType()));
                fields.put(current);
            }
            vectorLayer.put(NGWKEY_FIELDS, fields);
            payload.put(NGWKEY_VECTOR_LAYER, vectorLayer);
        } catch (JSONException e) {
            e.printStackTrace();
            return new HttpResponse(500);
        }

        return createNewResource(layer.getContext(), connection, payload);
    }

    public static HttpResponse createNewLookupTable(
            Connection connection,
            NGWLookupTable table,
            long parentId,
            String keyName)
    {
        JSONObject payload = new JSONObject();
        try {
            JSONObject resource = new JSONObject();
            resource.put(NGWKEY_CLS, NGWKEY_LOOKUP_TABLE);
            JSONObject id = new JSONObject();
            id.put(JSON_ID_KEY, parentId);
            resource.put(NGWKEY_PARENT, id);
            resource.put(JSON_DISPLAY_NAME, table.getName());
            if (keyName != null) {
                resource.put(NGWKEY_KEYNAME, keyName);
            }
            payload.put(JSON_RESOURCE_KEY, resource);

            JSONObject itemsResmeta = new JSONObject();
            itemsResmeta.put(NGWKEY_ITEMS, new JSONObject());
            payload.put(NGWKEY_RESMETA, itemsResmeta);

            JSONObject jsonData = new JSONObject();
            Map<String, String> data = table.getData();
            if (data != null) {
                for (Map.Entry<String, String> entry : data.entrySet()) {
                    jsonData.put(entry.getKey(), entry.getValue());
                }
            }
            JSONObject items = new JSONObject();
            items.put(NGWKEY_ITEMS, jsonData);
            payload.put(NGWKEY_LOOKUP_TABLE, items);
        } catch (JSONException e) {
            e.printStackTrace();
            return new HttpResponse(500);
        }

        return createNewResource(table.getContext(), connection, payload);
    }
} public class NetworkUtil
{
    private static final String IP_ADDRESS = "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
            + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
            + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
            + "|[1-9][0-9]|[0-9]))(:[0-9]{1,5})?";
    public static final String URL_PATTERN = "^(?i)((ftp|https?)://)?(([\\da-z.-]+)\\.([a-z.]{2,6})|" + IP_ADDRESS + ")(:[0-9]{1,5})?(/\\S*)?$";

    public final ConnectivityManager mConnectionManager;
    public final TelephonyManager    mTelephonyManager;
    protected       long                mLastCheckTime;
    protected       Context             mContext;

    public final static int TIMEOUT_CONNECTION = 10000;
    public final static int TIMEOUT_SOCKET = 240000; // 180 sec

    public final static int ERROR_AUTH                = -401;
    public final static int ERROR_NETWORK_UNAVAILABLE = -1;
    public final static int ERROR_CONNECT_FAILED      = 0;
    public final static int ERROR_DOWNLOAD_DATA       = 1;

    public final static String HTTP_GET    = "GET";
    public final static String HTTP_POST   = "POST";
    public final static String HTTP_PUT    = "PUT";
    public final static String HTTP_DELETE = "DELETE";


    public NetworkUtil(Context context)
    {
        mConnectionManager =
                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        mLastCheckTime = Constants.NOT_FOUND;
        mContext = context;
    }


    public synchronized boolean isNetworkAvailable()
    {
        if (mConnectionManager == null) {
            return false;
        }

        NetworkInfo info = mConnectionManager.getActiveNetworkInfo();
        if (info == null) {
            return false;
        }

        return info.isConnected();
    }

    public static HttpURLConnection getProperConnection(String targetURL) throws IOException {
        URL url = new URL(targetURL);
        // Open a HTTP connection to the URL
        if (targetURL.startsWith("https://"))
            return (HttpsURLConnection) url.openConnection();
        else
            return (HttpURLConnection) url.openConnection();
    }


    public static HttpURLConnection getHttpConnection(
            String method,
            String targetURL,
            String username,
            String password)
            throws IOException {
        HttpURLConnection conn = getProperConnection(targetURL);

        String basicAuth = getHTTPBaseAuth(username, password);
        if (null != basicAuth) {
            conn.setRequestProperty("Authorization", basicAuth);
        }

        return getHttpConnection(method, targetURL, conn);
    }

    public static HttpURLConnection getHttpConnection(
            String method,
            String targetURL,
            String auth)
            throws IOException {
        HttpURLConnection conn = getProperConnection(targetURL);

        if (null != auth) {
            conn.setRequestProperty("Authorization", auth);
        }

        return getHttpConnection(method, targetURL, conn);
    }


    public static HttpURLConnection getHttpConnection(
            String method,
            String targetURL,
            HttpURLConnection conn)
            throws IOException {
        conn.setRequestProperty("User-Agent", Constants.APP_USER_AGENT);

        // Allow Inputs
        conn.setDoInput(true);
        // Don't use a cached copy.
        conn.setUseCaches(false);
        // Use a post method.
        if (method.length() > 0)
            conn.setRequestMethod(method);

        conn.setConnectTimeout(TIMEOUT_CONNECTION);
        conn.setReadTimeout(TIMEOUT_SOCKET);
        conn.setRequestProperty("Accept", "*/*");

        return isValidUri(targetURL) ? conn : null;
    }


    public static boolean isValidUri(String url) {
        Pattern pattern = Pattern.compile(URL_PATTERN);
        Matcher match = pattern.matcher(url);
        return match.matches();
    }

    public static String getHTTPBaseAuth(String username, String password){
        if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
            return "Basic " + Base64.encodeToString(
                    (username + ":" + password).getBytes(), Base64.NO_WRAP);
        }
        return null;
    }

    public static String responseToString(final InputStream is)
            throws IOException
    {
        if (is == null) {
            return null;
        }

        byte[] buffer = new byte[Constants.IO_BUFFER_SIZE];
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        FileUtil.copyStream(is, baos, buffer, Constants.IO_BUFFER_SIZE);
        byte[] bytesReceived = baos.toByteArray();
        baos.close();
        is.close();

        return new String(bytesReceived);
    }

    public static void getStream(
            String targetURL,
            String username,
            String password,
            OutputStream outputStream)
            throws IOException
    {
        final HttpURLConnection conn = getHttpConnection("GET", targetURL, username, password);
        if (null == conn) {
            if (Constants.DEBUG_MODE)
                Log.d(TAG, "Error get stream: " + targetURL);
            throw new IOException("Connection is null");
        }
        int responseCode = conn.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_MOVED_PERM && conn.getURL().getProtocol().equals("http")) {
            targetURL = targetURL.replace("http", "https");
            getStream(targetURL, username, password, outputStream);
            return;
        }
        if (responseCode != HttpURLConnection.HTTP_OK) {
            if(Constants.DEBUG_MODE)
                Log.d(TAG, "Problem execute getStream: " + targetURL + " HTTP response: " +
                    responseCode + " username: " + username);
            throw new IOException("Response code is " + responseCode);
        }

        byte data[] = new byte[Constants.IO_BUFFER_SIZE];
        InputStream is = conn.getInputStream();
        FileUtil.copyStream(is, outputStream, data, Constants.IO_BUFFER_SIZE);
        outputStream.close();
    }

    protected static HttpResponse getHttpResponse(
            HttpURLConnection conn,
            boolean readErrorResponseBody)
            throws IOException
    {
        String method = conn.getRequestMethod();
        int code = conn.getResponseCode();
        String message = conn.getResponseMessage();
        HttpResponse response = new HttpResponse(code, message);

        if (code == HttpURLConnection.HTTP_MOVED_PERM && conn.getURL().getProtocol().equals("http")) {
            if (method.equals("PUT") || method.equals("POST")) {
                return response;
            }
            String target = conn.getURL().toString().replace("http", "https");
            String auth = conn.getRequestProperty("Authorization");
            HttpURLConnection connection = getHttpConnection(conn.getRequestMethod(), target, auth);
            return getHttpResponse(connection, readErrorResponseBody);
        }

        if (!(code == HttpURLConnection.HTTP_OK
                || (code == HttpURLConnection.HTTP_CREATED && method.equals(HTTP_POST)))) {
            if (Constants.DEBUG_MODE) {
                String url = conn.getURL().toExternalForm();
                if ("Keep-Alive".equals(conn.getRequestProperty("Connection")))
                    method = "postFile(), targetURL";
                Log.d(TAG, "Problem execute " + method + ": " + url + " HTTP response: " + code);
            }
            if (readErrorResponseBody)
                response.setResponseBody(responseToString(conn.getErrorStream()));
            return response;
        }

        String body = responseToString(conn.getInputStream());
        if (null == body) {
            response.setResponseCode(ERROR_DOWNLOAD_DATA);
            response.setResponseMessage(null);
            return response;
        }

        response.setResponseBody(body);
        response.setOk(true);
        return response;
    }

    public static HttpResponse get(
            String targetURL,
            String username,
            String password,
            boolean readErrorResponseBody)
            throws IOException {
        final HttpURLConnection conn = getHttpConnection(HTTP_GET, targetURL, username, password);
        if (null == conn) {
            if (Constants.DEBUG_MODE)
                Log.d(TAG, "Error get connection object: " + targetURL);
            return new HttpResponse(ERROR_CONNECT_FAILED);
        }
        return getHttpResponse(conn, readErrorResponseBody);
    }


    public static HttpResponse post(
            String targetURL,
            String payload,
            String username,
            String password,
            boolean readErrorResponseBody)
            throws IOException
    {
        final HttpURLConnection conn = getHttpConnection(HTTP_POST, targetURL, username, password);
        if (null == conn) {
            if (Constants.DEBUG_MODE)
                Log.d(TAG, "Error get connection object: " + targetURL);
            return new HttpResponse(ERROR_CONNECT_FAILED);
        }
        conn.setRequestProperty("Content-type", "application/json");
        // Allow Outputs
        conn.setDoOutput(true);

        OutputStream os = conn.getOutputStream();
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
        writer.write(payload);

        writer.flush();
        writer.close();
        os.close();

        HttpResponse response = getHttpResponse(conn, readErrorResponseBody);
        if (response.mResponseCode == HttpURLConnection.HTTP_MOVED_PERM && conn.getURL().getProtocol().equals("http")) {
            targetURL = targetURL.replace("http", "https");
            return post(targetURL, payload, username, password, readErrorResponseBody);
        }
        return response;
    }

    public static HttpResponse delete(
            String targetURL,
            String username,
            String password,
            boolean readErrorResponseBody)
            throws IOException
    {
        final HttpURLConnection conn = getHttpConnection(HTTP_DELETE, targetURL, username, password);
        if (null == conn) {
            if (Constants.DEBUG_MODE)
                Log.d(TAG, "Error get connection object: " + targetURL);
            return new HttpResponse(ERROR_CONNECT_FAILED);
        }
        return getHttpResponse(conn, readErrorResponseBody);
    }


    public static HttpResponse put(
            String targetURL,
            String payload,
            String username,
            String password,
            boolean readErrorResponseBody)
            throws IOException
    {
        final HttpURLConnection conn = getHttpConnection(HTTP_PUT, targetURL, username, password);
        if (null == conn) {
            if (Constants.DEBUG_MODE)
                Log.d(TAG, "Error get connection object: " + targetURL);
            return new HttpResponse(ERROR_CONNECT_FAILED);
        }
        conn.setRequestProperty("Content-type", "application/json");
        // Allow Outputs
        conn.setDoOutput(true);

        OutputStream os = conn.getOutputStream();
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
        writer.write(payload);

        writer.flush();
        writer.close();
        os.close();

        HttpResponse response = getHttpResponse(conn, readErrorResponseBody);
        if (response.mResponseCode == HttpURLConnection.HTTP_MOVED_PERM && conn.getURL().getProtocol().equals("http")) {
            targetURL = targetURL.replace("http", "https");
            return put(targetURL, payload, username, password, readErrorResponseBody);
        }
        return response;
    }


    public static HttpResponse postFile(
            String targetURL,
            String fileName,
            File file,
            String fileMime,
            String username,
            String password,
            boolean readErrorResponseBody)
            throws IOException
    {
        final String lineEnd = "\r\n";
        final String twoHyphens = "--";
        final String boundary = "**nextgis**";

        //------------------ CLIENT REQUEST
        FileInputStream fileInputStream = new FileInputStream(file);
        // open a URL connection to the Servlet

        HttpURLConnection conn = getHttpConnection(HTTP_POST, targetURL, username, password);
        if (null == conn) {
            if (Constants.DEBUG_MODE)
                Log.d(TAG, "Error get connection object: " + targetURL);
            return new HttpResponse(ERROR_CONNECT_FAILED);
        }
        conn.setRequestProperty("Connection", "Keep-Alive");
        conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);
        // Allow Outputs
        conn.setDoOutput(true);

        DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
        dos.writeBytes(twoHyphens + boundary + lineEnd);
        dos.writeBytes(
                "Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName + "\"" +
                lineEnd);

        if (!TextUtils.isEmpty(fileMime)) {
            dos.writeBytes("Content-Type: " + fileMime + lineEnd);
        }

        dos.writeBytes(lineEnd);

        byte[] buffer = new byte[Constants.IO_BUFFER_SIZE];
        FileUtil.copyStream(fileInputStream, dos, buffer, Constants.IO_BUFFER_SIZE);

        dos.writeBytes(lineEnd);
        dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
        fileInputStream.close();
        dos.flush();
        dos.close();

        HttpResponse response = getHttpResponse(conn, readErrorResponseBody);
        if (response.mResponseCode == HttpURLConnection.HTTP_MOVED_PERM && conn.getURL().getProtocol().equals("http")) {
            targetURL = targetURL.replace("http", "https");
            return postFile(targetURL, fileName, file, fileMime, username, password, readErrorResponseBody);
        }
        return response;
    }

    public static String trimSlash(String url) {
        return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
    }

    public static String getError(
            Context context,
            int responseCode)
    {
        switch (responseCode) {
            case ERROR_AUTH:
                return context.getString(R.string.error_auth);
            case ERROR_NETWORK_UNAVAILABLE:
                return context.getString(R.string.error_network_unavailable);
            case ERROR_CONNECT_FAILED:
                return context.getString(R.string.error_connect_failed);
            case ERROR_DOWNLOAD_DATA:
                return context.getString(R.string.error_download_data);
            case HttpURLConnection.HTTP_UNAUTHORIZED:
                return context.getString(R.string.error_401);
            case HttpURLConnection.HTTP_FORBIDDEN:
                return context.getString(R.string.error_403);
            case HttpURLConnection.HTTP_NOT_FOUND:
                return context.getString(R.string.error_404);
            case HttpURLConnection.HTTP_INTERNAL_ERROR:
                return context.getString(R.string.error_500);
            default:
                return context.getString(R.string.error_500);
        }
    }
}
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteFullException;
import android.graphics.Color;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.nextgis.maplib.R;
import com.nextgis.maplib.api.IGISApplication;
import com.nextgis.maplib.api.IGeometryCache;
import com.nextgis.maplib.api.IGeometryCacheItem;
import com.nextgis.maplib.api.IJSONStore;
import com.nextgis.maplib.api.IProgressor;
import com.nextgis.maplib.api.IStyleRule;
import com.nextgis.maplib.datasource.Feature;
import com.nextgis.maplib.datasource.Field;
import com.nextgis.maplib.datasource.GeoEnvelope;
import com.nextgis.maplib.datasource.GeoGeometry;
import com.nextgis.maplib.datasource.GeoGeometryFactory;
import com.nextgis.maplib.datasource.GeoMultiPoint;
import com.nextgis.maplib.datasource.GeoMultiPolygon;
import com.nextgis.maplib.datasource.GeoPoint;
import com.nextgis.maplib.datasource.GeoPolygon;
import com.nextgis.maplib.datasource.GeometryRTree;
import com.nextgis.maplib.datasource.ngw.Connection;
import com.nextgis.maplib.display.FieldStyleRule;
import com.nextgis.maplib.display.RuleFeatureRenderer;
import com.nextgis.maplib.display.SimpleFeatureRenderer;
import com.nextgis.maplib.display.SimpleLineStyle;
import com.nextgis.maplib.display.SimpleMarkerStyle;
import com.nextgis.maplib.display.SimplePolygonStyle;
import com.nextgis.maplib.display.Style;
import com.nextgis.maplib.util.AttachItem;
import com.nextgis.maplib.util.Constants;
import com.nextgis.maplib.util.FeatureChanges;
import com.nextgis.maplib.util.FileUtil;
import com.nextgis.maplib.util.GeoConstants;
import com.nextgis.maplib.util.GeoJSONUtil;
import com.nextgis.maplib.util.LayerUtil;
import com.nextgis.maplib.util.MapUtil;
import com.nextgis.maplib.util.NGException;
import com.nextgis.maplib.util.NGWUtil;
import com.nextgis.maplib.util.NetworkUtil;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.nextgis.maplib.util.Constants.CHANGE_OPERATION_CHANGED;
import static com.nextgis.maplib.util.Constants.CHANGE_OPERATION_DELETE;
import static com.nextgis.maplib.util.Constants.CHANGE_OPERATION_NEW;
import static com.nextgis.maplib.util.Constants.DEBUG_MODE;
import static com.nextgis.maplib.util.Constants.FIELD_GEOM;
import static com.nextgis.maplib.util.Constants.FIELD_ID;
import static com.nextgis.maplib.util.Constants.JSON_NAME_KEY;
import static com.nextgis.maplib.util.Constants.JSON_RENDERERPROPS_KEY;
import static com.nextgis.maplib.util.Constants.JSON_STYLE_RULE_KEY;
import static com.nextgis.maplib.util.Constants.LAYERTYPE_LOCAL_VECTOR;
import static com.nextgis.maplib.util.Constants.MAX_CONTENT_LENGTH;
import static com.nextgis.maplib.util.Constants.MIN_LOCAL_FEATURE_ID;
import static com.nextgis.maplib.util.Constants.NOT_FOUND;
import static com.nextgis.maplib.util.Constants.TAG;
import static com.nextgis.maplib.util.Constants.URI_ATTACH;
import static com.nextgis.maplib.util.Constants.URI_PARAMETER_NOT_SYNC;
import static com.nextgis.maplib.util.Constants.URI_PARAMETER_TEMP;
import static com.nextgis.maplib.util.GeoConstants.FTDate;
import static com.nextgis.maplib.util.GeoConstants.FTDateTime;
import static com.nextgis.maplib.util.GeoConstants.FTInteger;
import static com.nextgis.maplib.util.GeoConstants.FTReal;
import static com.nextgis.maplib.util.GeoConstants.FTString;
import static com.nextgis.maplib.util.GeoConstants.FTTime;
import static com.nextgis.maplib.util.GeoConstants.GTLineString;
import static com.nextgis.maplib.util.GeoConstants.GTMultiLineString;
import static com.nextgis.maplib.util.GeoConstants.GTMultiPoint;
import static com.nextgis.maplib.util.GeoConstants.GTMultiPolygon;
import static com.nextgis.maplib.util.GeoConstants.GTNone;
import static com.nextgis.maplib.util.GeoConstants.GTPoint;
import static com.nextgis.maplib.util.GeoConstants.GTPolygon;
/*
 * Project:  NextGIS Mobile
 * Purpose:  Mobile GIS for Android.
 * Author:   Dmitry Baryshnikov (aka Bishop), bishop.dev@gmail.com
 * Author:   NikitaFeodonit, nfeodonit@yandex.com
 * Author:   Stanislav Petriakov, becomeglory@gmail.com
 * *****************************************************************************
 * Copyright (c) 2012-2019 NextGIS, info@nextgis.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.nextgis.maplib.map;







/**
 * The vector layer class. It stores geometry and attributes.
 * <p/>
 * Here some examples of code to work with this layer.
 * <p/>
 * <b>To put image to the feature</b> <code> Uri newUri = ... content://com.nextgis.mobile.provider/layer_xxxxxxxxx/1/attach
 * Uri uri = getContentResolver().insert(newUri, null); try { OutputStream outStream =
 * getContentResolver().openOutputStream(uri); sourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50,
 * outStream); outStream.close(); } catch (Exception e) { Log.e(TAG, "exception while writing
 * image", e); } </code>
 * <p/>
 * <b>To get bitmap from uri</b> <code> private Bitmap getBitmapFromUri(Uri uri) throws IOException
 * { ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r");
 * FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); Bitmap image =
 * BitmapFactory.decodeFileDescriptor(fileDescriptor); parcelFileDescriptor.close(); return image; }
 * </code>
 * <p/>
 * <b>To get image using uri</b> <code> Uri featureUri = content://com.nextgis.mobile.provider/layer_xxxxxxxxx/1
 * Uri thisAttachUri = ContentUris.withAppendedId(featureUri, attachId); InputStream inStream =
 * null; try { inStream = resolver.openInputStream(thisAttachUri); // what to do with the stream is
 * up to you // I simply create a bitmap to display it Bitmap bm = BitmapFactory.decodeStream(inStream);
 * FrameLayout frame = (FrameLayout)findViewById(R.id.picture_frame); ImageView view = new
 * ImageView(getApplicationContext()); view.setImageBitmap(bm); frame.addView(view); } catch
 * (FileNotFoundException e) { Log.e(TAG, "file not found " + thisAttachUri, e); } finally { if
 * (inStream != null) { try { inStream.close(); } catch (IOException e) { Log.e(TAG, "could not
 * close stream", e); } } </code>
 * <p/>
 * <b>Also it can be used</b> <code> Uri newUri = ... content://com.nextgis.mobile.provider/layer_xxxxxxxxx/1/attach
 * Cursor cursor = resolver.query(newUri, {MediaStore.MediaColumns.DATA}, null....) File bitmapFile
 * = new File(cursor.getString(ATTACH_DATA)) and open using real path </code>
 *
 * @author Dmitry Baryshnikov <dmitry.baryshnikov@nextgis.com>
 */

public class VectorLayer
        extends Layer
{
    protected static final String JSON_GEOMETRY_TYPE_KEY = "geometry_type";
    protected static final String JSON_FIELDS_KEY        = "fields";
    protected static final String JSON_EDITABLE_KEY      = "is_editable";

    protected static final String CONTENT_ATTACH_TYPE = "vnd.android.cursor.dir/*";
    protected static final String NO_SYNC             = "no_sync";

    protected static final int TYPE_TABLE     = 1;
    protected static final int TYPE_FEATURE   = 2;
    protected static final int TYPE_ATTACH    = 3;
    protected static final int TYPE_ATTACH_ID = 4;

    protected static final String META  = "meta.json";
    protected static final String RTREE = "rtree";

    public static final String ATTACH_DISPLAY_NAME = MediaStore.MediaColumns.DISPLAY_NAME;
    public static final String ATTACH_SIZE         = MediaStore.MediaColumns.SIZE;
    public static final String ATTACH_ID           = MediaStore.MediaColumns._ID;
    public static final String ATTACH_MIME_TYPE    = MediaStore.MediaColumns.MIME_TYPE;
    public static final String ATTACH_DATA         = MediaStore.MediaColumns.DATA;
    public static final String ATTACH_DATE_ADDED   = MediaStore.MediaColumns.DATE_ADDED;
    public static final String ATTACH_DESCRIPTION  = MediaStore.Images.ImageColumns.DESCRIPTION;

    public static final int COLUMN_TYPE_UNKNOWN = 0;
    public static final int COLUMN_TYPE_STRING  = 1;
    public static final int COLUMN_TYPE_LONG    = 2;

    protected static String CONTENT_TYPE;
    protected static String CONTENT_ITEM_TYPE;

    protected static String     mAuthority;
    protected static UriMatcher mUriMatcher;

    protected Map<String, Field> mFields;

    protected boolean mCacheLoaded, mIsCacheRebuilding;
    protected int     mGeometryType;
    protected long    mUniqId;
    protected boolean mIsLocked;

    protected boolean mIsEditable;

    /**
     * The geometry cache for fast querying and drawing
     */
    protected IGeometryCache mCache;