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 ILayer
{
    /**
     * @return Application context
     */
    Context getContext();

    /**
     * @return User readable layer name
     */
    String getName();

    /**
     * Set layer name
     * @param newName New name
     */
    void setName(String newName);

    /**
     * @return Layer identofoctor - set by map on current session
     */
    int getId();

    /**
     * Get Layer type (@see com.nextgis.maplib.util.Constants)
     * @return Layer type
     */
    int getType();

    /**
     * Delete layer
     * @return true on success or false
     */
    boolean delete();

    /**
     * Get layer path in storage
     * @return Layer path
     */
    File getPath();

    /**
     * Save layer changes
     * @return true on success or false
     */
    boolean save();

    /**
     * Load layer
     * @return true on  success or false
     */
    boolean load();

    /**
     * Get layer extents
     * @return Layer extents in map coordinates
     */
    GeoEnvelope getExtents();

    /**
     * set layer parent
     * @param layer Layer parent object
     */
    void setParent(ILayer layer);

    /**
     * @return Layer parent object
     */
    ILayer getParent();

    /**
     * Set layer internal identifictor - set by map on current session
     * @param id New layer identificator
     */
    void setId(int id);

    /**
     * @return Is layer valid (all data are present, .etc.)
     */
    boolean isValid();

    /**
     * Triggered on layer contents or properties changes
     */
    void notifyUpdateAll();

    /**
     * Triggered on layer contents changed
     * @param rowId New record id
     * @param oldRowId Old record id
     * @param attributesOnly
     */
    void notifyUpdate(long rowId, long oldRowId, boolean attributesOnly);

    /**
     * Triggered on layer added new record
     * @param rowId New record id
     */
    void notifyInsert(long rowId);

    /**
     * Triggered on layer delete all records
     */
    void notifyDeleteAll();

    /**
     * Triggered on layer delete record
     * @param rowId Deleted record id
     */
    void notifyDelete(long rowId);

    /**
     * Executed then database version is changes. Triggered on application upgrade
     * @param sqLiteDatabase The database
     * @param oldVersion Old database version
     * @param newVersion New database version
     */
    void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion);
} public interface INGWLayer
{
    /**
     * Return the account connected with this layer
     * @return account name
     */
    String getAccountName();

    /**
     * Set the account connected with this layer
     * @param account Name
     */
    void setAccountName(String account);

    /**
     * cache account data for fast access
     */
    void setAccountCacheData();

    /**
     * Sync layer data with NextGIS Web
     * @param authority the account authority
     * @param syncResult object for storing results of operations
     */
    void sync(String authority, Pair<Integer, Integer> ver, SyncResult syncResult);

    /**
     * Return the sync type (i.e. sync only attributes or the whole feature)
     * @return
     */
    int getSyncType();

    /**
     * Set sync type.
     * @see com.nextgis.maplib.util.Constants#SYNC_ALL
     * @param syncType The value result of OR different sync types
     */
    void setSyncType(int syncType);

    /**
     * Return the NextGIS Web server identificator - uniq identificator set for layer on server.
     * @return The NextGIS Web server identificator
     */
    long getRemoteId();

    /**
     * Set NextGIS Web server identificator.
     * @param remoteId The NextGIS Web server identificator
     */
    void setRemoteId(long remoteId);
} public class LayerGroup
        extends Layer
{
    protected final List<ILayer> mLayers = new ArrayList<>();
    protected LayerFactory mLayerFactory;
    protected int          mLayerDrawIndex;
    protected GISDisplay   mDisplay;
    protected OnAllLayersAddedListener mOnAllLayersAddedListener;


    public interface OnAllLayersAddedListener
    {
        void onAllLayersAdded(List<ILayer> layers);
    }


    public LayerGroup(
            Context context,
            File path,
            LayerFactory layerFactory)
    {
        super(context, path);

        mLayerFactory = layerFactory;

        mLayerDrawIndex = 0;

        mLayerType = LAYERTYPE_GROUP;
    }


    /**
     * Get layer by identificator
     *
     * @param id
     *         Layer identificator
     *
     * @return Layer or null
     */
    public ILayer getLayerById(int id)
    {
        if (mId == id) {
            return this;
        }

        for (ILayer layer : mLayers) {
            if (layer.getId() == id) {
                return layer;
            }
        }
        return null;
    }


    /**
     * search layer by it human readabler name
     * @param name Name to search
     * @return ILayer or null
     */
    public ILayer getLayerByName(String name)
    {
        if (mName.equals(name)) {
            return this;
        }

        for (ILayer layer : mLayers) {
            if (layer.getName().equals(name)) {
                return layer;
            }
        }
        return null;
    }

    /**
     * Search layer by it folder name
     * @param name Name to search
     * @return ILayer or null
     */
    public ILayer getLayerByPathName(String name){
        if (getPath().getName().equals(name)) {
            return this;
        }

        for (ILayer layer : mLayers) {
            if (layer.getPath().getName().equals(name)) {
                return layer;
            }
        }
        return null;
    }


    /**
     * Get a list of specified type layers
     *
     * @param layerGroup
     *         to inspect for layers
     * @param types
     *         A layer type
     * @param layerList
     *         A list to fill with find layers
     */
    public static void getLayersByType(
            LayerGroup layerGroup,
            int types,
            List<ILayer> layerList)
    {
        for (int i = 0; i < layerGroup.getLayerCount(); i++) {
            ILayer layer = layerGroup.getLayer(i);

            if (0 != (types & layer.getType())) {
                layerList.add(layer);
            }

            if (layer instanceof LayerGroup) {
                getLayersByType((LayerGroup) layer, types, layerList);
            }
        }
    }


    public static void getVectorLayersByType(
            LayerGroup layerGroup,
            int types,
            List<ILayer> layerList)
    {
        for (int i = 0; i < layerGroup.getLayerCount(); i++) {
            ILayer layer = layerGroup.getLayer(i);

            if (layer instanceof VectorLayer) {
                VectorLayer vectorLayer = (VectorLayer) layer;
                if (0 != (types & 1 << vectorLayer.getGeometryType())) {
                    layerList.add(0, layer);
                }
            }

            if (layer instanceof LayerGroup) {
                getVectorLayersByType((LayerGroup) layer, types, layerList);
            }
        }
    }


    /**
     * Create existed layer from path and add it to the map
     *
     * @param layer
     *         A layer object
     */
    public void addLayer(ILayer layer)
    {
        if (layer != null) {
            mLayers.add(layer);
            layer.setParent(this);
            onLayerAdded(layer);
        }
    }


    public void insertLayer(
            int index,
            ILayer layer)
    {
        if (layer != null) {

            mLayers.add(index, layer);
            layer.setParent(this);
            onLayerAdded(layer);
        }
    }


    public void moveLayer(
            int newPosition,
            ILayer layer)
    {
        if (layer != null) {

            synchronized (this) {
                mLayers.remove(layer);
                mLayers.add(newPosition, layer);
            }
            onLayersReordered();
        }
    }


    public int removeLayer(ILayer layer)
    {
        synchronized (this) {
            int result = mLayers.size() - 1;

            if (layer != null) {
                result = mLayers.indexOf(layer);
                onLayerDeleted(layer.getId());
            }
            return result;
        }
    }


    @Override
    public void runDraw(GISDisplay display)
    {
        if (null != display && mDisplay != display) {
            mDisplay = display;
        }

        if (mLayers.size() == 0) {
            return;
        }

        //synchronized (this) {
            for (ILayer layer : mLayers) {
                if (Thread.currentThread().isInterrupted()) {
                    break;
                }

                if (layer instanceof LayerGroup) {
                    LayerGroup layerGroup = (LayerGroup) layer;
                    layerGroup.runDraw(mDisplay);

                } else {

                    if (layer.isValid() && layer instanceof ILayerView) {

                        ILayerView layerView = (ILayerView) layer;
                        if (layerView.isVisible() && layer instanceof IRenderer &&
                                mDisplay.getZoomLevel() <= layerView.getMaxZoom() &&
                                mDisplay.getZoomLevel() >= layerView.getMinZoom()) {
                            // Log.d(Constants.TAG, "Layer Draw Index: " + mLayerDrawIndex);

                            IRenderer renderer = (IRenderer) layer;
                            renderer.runDraw(mDisplay);

                        }
                    }
                }
            }
        //}
    }


    @Override
    public void cancelDraw()
    {
        for (ILayer layer : mLayers) {
            if (layer instanceof IRenderer) {
                IRenderer renderer = (IRenderer) layer;
                renderer.cancelDraw();
            }
        }
    }


    @Override
    public boolean isVisible()
    {
        for (ILayer layer : mLayers) {
            if (layer instanceof ILayerView) {
                ILayerView layerView = (ILayerView) layer;
                if (layerView.isVisible()) {
                    return true;
                }
            }
        }
        return false;
    }


    @Override
    public void setVisible(boolean visible)
    {
        for (ILayer layer : mLayers) {
            if (layer instanceof ILayerView) {
                ILayerView layerView = (ILayerView) layer;
                layerView.setVisible(visible);
            }
        }
    }


    public int getVisibleTopLayerId()
    {
       for (int i = mLayers.size() - 1; i >= 0; i--) {
            ILayer layer = mLayers.get(i);
            if (layer instanceof LayerGroup) {
                LayerGroup layerGroup = (LayerGroup) layer;
                int visibleTopLayerId = layerGroup.getVisibleTopLayerId();
                if (Constants.NOT_FOUND != visibleTopLayerId) {
                    return visibleTopLayerId;
                }

            } else {
                if (layer.isValid() && layer instanceof ILayerView) {
                    ILayerView layerView = (ILayerView) layer;
                    if (layerView.isVisible()) {
                        return layer.getId();
                    }
                }
            }
        }

        return Constants.NOT_FOUND;
    }


    public int getVisibleLayerCount()
    {
        int visibleLayerCount = 0;

        for (int i = mLayers.size() - 1; i >= 0; i--) {
            ILayer layer = mLayers.get(i);
            if (layer instanceof LayerGroup) {
                LayerGroup layerGroup = (LayerGroup) layer;
                visibleLayerCount += layerGroup.getVisibleLayerCount();

            } else {
                if (layer.isValid() && layer instanceof ILayerView) {
                    ILayerView layerView = (ILayerView) layer;
                    if (layerView.isVisible()) {
                        ++visibleLayerCount;
                    }
                }
            }
        }

        return visibleLayerCount;
    }


    @Override
    public boolean delete()
    {
        for (ILayer layer : mLayers) {
            layer.setParent(null);
            layer.delete();
        }
        return super.delete();
    }


    @Override
    public JSONObject toJSON()
            throws JSONException
    {
        JSONObject rootConfig = super.toJSON();

        JSONArray jsonArray = new JSONArray();
        rootConfig.put(JSON_LAYERS_KEY, jsonArray);
        for (ILayer layer : mLayers) {
            JSONObject layerObject = new JSONObject();
            layerObject.put(JSON_PATH_KEY, layer.getPath().getName());
            jsonArray.put(layerObject);
        }
        return rootConfig;
    }


    public void clearLayers()
    {
        for (ILayer layer : mLayers) {
            if (layer instanceof LayerGroup) {
                ((LayerGroup) layer).clearLayers();
            }
        }

        mLayers.clear();
    }


    @Override
    public void fromJSON(JSONObject jsonObject)
            throws JSONException
    {
        super.fromJSON(jsonObject);

        clearLayers();

        final JSONArray jsonArray = jsonObject.getJSONArray(JSON_LAYERS_KEY);
        for (int i = 0; i < jsonArray.length(); i++) {
            JSONObject jsonLayer = jsonArray.getJSONObject(i);
            String sPath = jsonLayer.getString(JSON_PATH_KEY);
            File inFile = new File(getPath(), sPath);
            if (inFile.exists()) {
                ILayer layer = mLayerFactory.createLayer(mContext, inFile);
                if (null != layer && layer.load()) {
                    addLayer(layer);
                }
            }
        }

        if (mOnAllLayersAddedListener != null)
            mOnAllLayersAddedListener.onAllLayersAdded(mLayers);
    }


    @Override
    public GeoEnvelope getExtents()
    {
        return mExtents;
    }


    @Override
    public void onUpgrade(final SQLiteDatabase sqLiteDatabase, final int oldVersion, final int newVersion) {
        setOnAllLayersAddedListener(new OnAllLayersAddedListener() {
            @Override
            public void onAllLayersAdded(List<ILayer> layers) {
                for (ILayer layer : mLayers) {
                        layer.onUpgrade(sqLiteDatabase, oldVersion, newVersion);
                        setOnAllLayersAddedListener(null);
                }
            }
        });
    }


    protected void setOnAllLayersAddedListener(OnAllLayersAddedListener listener) {
        mOnAllLayersAddedListener = listener;
    }


    protected void onLayerAdded(ILayer layer)
    {
        if (mParent != null && mParent instanceof LayerGroup) {
            LayerGroup group = (LayerGroup) mParent;
            group.onLayerAdded(layer);
        }
    }


    protected void onLayerChanged(ILayer layer)
    {
        if (mParent != null && mParent instanceof LayerGroup) {
            LayerGroup group = (LayerGroup) mParent;
            group.onLayerChanged(layer);
        }
    }


    protected void onLayerDeleted(int id)
    {
        for (ILayer layer : mLayers) {
            if (layer.getId() == id) {
                mLayers.remove(layer);
                break;
            }
        }

        if (mParent != null && mParent instanceof LayerGroup) {
            LayerGroup group = (LayerGroup) mParent;
            group.onLayerDeleted(id);
        }
    }


    protected void onExtentChanged(
            float zoom,
            GeoPoint center)
    {
        if (mParent != null && mParent instanceof LayerGroup) {
            LayerGroup group = (LayerGroup) mParent;
            group.onExtentChanged(zoom, center);
        }
    }


    protected void onLayersReordered()
    {
        if (mParent != null && mParent instanceof LayerGroup) {
            LayerGroup group = (LayerGroup) mParent;
            group.onLayersReordered();
        }
    }


    public int getLayerCount()
    {
        return mLayers.size();
    }


    public ILayer getLayer(int index)
    {
        return mLayers.get(index);
    }


    public LayerFactory getLayerFactory()
    {
        return mLayerFactory;
    }


    @Override
    public boolean save()
    {
        synchronized (this) {
            for (ILayer layer : mLayers) {
                layer.save();
            }
        }
        return super.save();
    }

    /**
     * Create the layer folder of specified name
     * @param layerName The name of folder
     * @return Path to the layer folder
     */
    public File createLayerStorage(String layerName)
    {
        if(TextUtils.isEmpty(layerName))
            return createLayerStorage();
        return new File(mPath, layerName);
    }

    /**
     * Create the layer folder of random name
     * @return Path to the layer folder
     */
    public File createLayerStorage()
    {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
        String layerDir = LAYER_PREFIX + sdf.format(new Date()) + getLayerCount();
        final Random r = new Random();
        layerDir += r.nextInt(99);

        Log.d(Constants.TAG, "createLayerStorage: " + layerDir);
        return new File(mPath, layerDir);
    }


    @Override
    public void setViewSize(
            int w,
            int h)
    {
        super.setViewSize(w, h);

        for (ILayer layer : mLayers) {
            if (layer instanceof ILayerView) {
                ILayerView lv = (ILayerView) layer;
                lv.setViewSize(w, h);
            }
        }
    }


    public boolean isChanges()
    {
        for (ILayer layer : mLayers) {
            if (layer instanceof LayerGroup) {
                LayerGroup layerGroup = (LayerGroup) layer;
                if (layerGroup.isChanges()) {
                    return true;
                }
            } else if (layer instanceof VectorLayer) {
                VectorLayer vectorLayer = (VectorLayer) layer;
                if (vectorLayer.isChanges()) {
                    return true;
                }
            }
        }
        return false;
    }


    public boolean haveFeaturesNotSyncFlag()
    {
        for (ILayer layer : mLayers) {
            if (layer instanceof LayerGroup) {
                LayerGroup layerGroup = (LayerGroup) layer;
                if (layerGroup.haveFeaturesNotSyncFlag()) {
                    return true;
                }
            } else if (layer instanceof VectorLayer) {
                VectorLayer vectorLayer = (VectorLayer) layer;
                if (vectorLayer.haveFeaturesNotSyncFlag()) {
                    return true;
                }
            }
        }
        return false;
    }
} public class MapBase
        extends LayerGroup
{
    protected int mNewId;
    protected static MapBase mInstance = null;
    protected String mFileName;
    private boolean mDirty;

    public MapBase(
            Context context,
            File path,
            LayerFactory layerFactory)
    {
        super(context, path.getParentFile(), layerFactory);
        mNewId = 0;
        mId = NOT_FOUND;
        mInstance = this;
        mFileName = path.getName();
    }


    /**
     * The identificator generator
     *
     * @return new id
     */
    public int getNewId()
    {
        return mNewId++;
    }


    @Override
    protected void onLayerAdded(ILayer layer)
    {
        layer.setId(getNewId());
        super.onLayerAdded(layer);
    }


    public static MapBase getInstance()
    {
        if (mInstance == null) {
            throw new IllegalArgumentException(
                    "Impossible to get the instance. This class must be initialized before");
        }
        return mInstance;
    }


    public ILayer getLastLayer()
    {
        if (mLayers.size() == 0) {
            return null;
        }
        return mLayers.get(mLayers.size() - 1);
    }


    @Override
    public boolean delete()
    {
        for (ILayer layer : mLayers) {
            layer.setParent(null);
            layer.delete();
        }

        mLayers.clear();

        return FileUtil.deleteRecursive(getFileName());
    }


    @Override
    protected File getFileName()
    {
        return new File(getPath(), mFileName);
    }


    public void moveTo(File newPath)
    {

        if (mPath.equals(newPath)) {
            return;
        }

        clearLayers();

        if (FileUtil.move(mPath, newPath)) {
            //change path
            mPath = newPath;
        }
        load();
    }

    public GeoEnvelope getFullBounds(){
        if(null != mDisplay){
            return mDisplay.getFullBounds();
        }
        return new GeoEnvelope();
    }

    public GeoEnvelope getCurrentBounds()
    {
        if (mDisplay != null) {
            return mDisplay.getBounds();
        }
        return null;
    }

    @Override
    public void clearLayers() {
        super.clearLayers();
        mNewId = 0;
    }

    public void setDirty(boolean dirty) {
        mDirty = dirty;
    }

    public boolean isDirty() {
        return mDirty;
    }
} public class MapContentProviderHelper
        extends MapBase
{
    protected DatabaseHelper mDatabaseHelper;

    protected static final String DBNAME           = "layers";
    protected static final int    DATABASE_VERSION = 4;


    public MapContentProviderHelper(
            Context context,
            File path,
            LayerFactory layerFactory)
    {
        super(context, path, layerFactory);

        File dbFullName;
        SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(context);
        File defaultPath = getContext().getExternalFilesDir(KEY_PREF_MAP);
        if (defaultPath != null) {
            String mapPath = sharedPreferences.getString(KEY_PREF_MAP_PATH, defaultPath.getPath());
            dbFullName = new File(mapPath, DBNAME);
        } else {
            dbFullName = context.getDatabasePath(DBNAME);
        }

        mDatabaseHelper = new DatabaseHelper(
                context,          // the application context
                dbFullName,       // the name of the database
                null,             // uses the default SQLite cursor
                DATABASE_VERSION  // the version number
        );

        // register events from layers modify in services or other applications
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(Constants.NOTIFY_DELETE);
        intentFilter.addAction(Constants.NOTIFY_DELETE_ALL);
        intentFilter.addAction(Constants.NOTIFY_INSERT);
        intentFilter.addAction(Constants.NOTIFY_UPDATE);
        intentFilter.addAction(Constants.NOTIFY_UPDATE_ALL);
        intentFilter.addAction(Constants.NOTIFY_UPDATE_FIELDS);
        intentFilter.addAction(Constants.NOTIFY_FEATURE_ID_CHANGE);

        context.registerReceiver(new VectorLayerNotifyReceiver(), intentFilter);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        super.onUpgrade(sqLiteDatabase, oldVersion, newVersion);

        Cursor data;
        boolean tableExists = false;

        try {
            data = sqLiteDatabase.query(TrackLayer.TABLE_TRACKS, null, null, null, null, null, null);
            tableExists = true;
            data.close();
        } catch (Exception ignored) { }

        if (oldVersion <= 2 && tableExists) {
            sqLiteDatabase.execSQL("alter table " + TrackLayer.TABLE_TRACKS + " add column " + TrackLayer.FIELD_COLOR + " integer;");

            GeoPoint point = new GeoPoint();
            ContentValues cv = new ContentValues();
            data = sqLiteDatabase.query(TrackLayer.TABLE_TRACKPOINTS,
                    new String[]{TrackLayer.FIELD_TIMESTAMP, TrackLayer.FIELD_LON, TrackLayer.FIELD_LAT}, null, null, null, null, null);

            if (data != null) {
                if (data.moveToFirst()) {
                    do {
                        point.setCoordinates(data.getDouble(1), data.getDouble(2));
                        point.setCRS(GeoConstants.CRS_WGS84);
                        point.project(GeoConstants.CRS_WEB_MERCATOR);
                        cv.clear();
                        cv.put(TrackLayer.FIELD_LON, point.getX());
                        cv.put(TrackLayer.FIELD_LAT, point.getY());
                        sqLiteDatabase.update(TrackLayer.TABLE_TRACKPOINTS, cv, TrackLayer.FIELD_TIMESTAMP + " = ?", new String[]{data.getLong(0) + ""});
                    } while (data.moveToNext());
                }

                data.close();
            }
        }

        if (oldVersion <= 3 && tableExists) {
            sqLiteDatabase.execSQL("alter table " + TrackLayer.TABLE_TRACKPOINTS + " add column " + TrackLayer.FIELD_SENT + " integer not null default 1;");
            sqLiteDatabase.execSQL("alter table " + TrackLayer.TABLE_TRACKPOINTS + " add column " + TrackLayer.FIELD_ACCURACY + " real;");
            sqLiteDatabase.execSQL("alter table " + TrackLayer.TABLE_TRACKPOINTS + " add column " + TrackLayer.FIELD_SPEED + " real;");
        }
    }

    public SQLiteDatabase getDatabase(boolean readOnly)
    {
        if (readOnly) {
            return mDatabaseHelper.getReadableDatabase();
        } else {
            return mDatabaseHelper.getWritableDatabase();
        }
    }


    /**
     * @param pathName
     *         The exact name of the folder which contains a layer. Must be without slashes.
     */
    public static Layer getVectorLayerByPath(
            LayerGroup layerGroup,
            String pathName)
    {
        for (int i = 0; i < layerGroup.getLayerCount(); i++) {
            ILayer layer = layerGroup.getLayer(i);
            if (layer instanceof LayerGroup) {
                LayerGroup inLayerGroup = (LayerGroup) layer;
                Layer out = getVectorLayerByPath(inLayerGroup, pathName);
                if (null != out) {
                    return out;
                }
            } else if (layer instanceof VectorLayer) {
                VectorLayer vectorLayer = (VectorLayer) layer;
                if (pathName.equals(vectorLayer.getPath().getName())) {
                    return vectorLayer;
                }
            } else if (layer instanceof TrackLayer) {
                TrackLayer trackLayer = (TrackLayer) layer;
                if (pathName.contains(TrackLayer.TABLE_TRACKS) ||
                    pathName.contains(TrackLayer.TABLE_TRACKPOINTS)) {
                    return trackLayer;
                }
            }
        }
        return null;
    }


    public static void getLayersByAccount(
            LayerGroup layerGroup,
            String account,
            List<INGWLayer> layerList)
    {
        for (int i = 0; i < layerGroup.getLayerCount(); i++) {
            ILayer layer = layerGroup.getLayer(i);

            if (layer instanceof INGWLayer) {
                INGWLayer ngwLayer = (INGWLayer) layer;
                if (ngwLayer.getAccountName().equals(account)) {
                    layerList.add(ngwLayer);
                }
            }

            if (layer instanceof LayerGroup) {
                getLayersByAccount((LayerGroup) layer, account, layerList);
            }
        }
    }


    public class VectorLayerNotifyReceiver
            extends BroadcastReceiver
    {

        @Override
        public void onReceive(
                Context context,
                Intent intent)
        {
            // extreme logging commented
            //Log.d(TAG, "Receive notify: " + intent.getAction());

            if(!intent.hasExtra(Constants.NOTIFY_LAYER_NAME))
                return;

            ILayer layer = getVectorLayerByPath(MapContentProviderHelper.this,
                    intent.getStringExtra(Constants.NOTIFY_LAYER_NAME));
            if(null == layer)
                return;

            switch (intent.getAction()) {

                case Constants.NOTIFY_DELETE:
                    layer.notifyDelete(intent.getLongExtra(FIELD_ID, NOT_FOUND));
                    break;

                case Constants.NOTIFY_DELETE_ALL:
                    layer.notifyDeleteAll();
                    break;

                case Constants.NOTIFY_UPDATE:
                case Constants.NOTIFY_UPDATE_FIELDS:
                    layer.notifyUpdate(
                            intent.getLongExtra(FIELD_ID, NOT_FOUND),
                            intent.getLongExtra(FIELD_OLD_ID, NOT_FOUND),
                            intent.getBooleanExtra(Constants.ATTRIBUTES_ONLY, true));
                    break;

                case Constants.NOTIFY_UPDATE_ALL:
                    layer.notifyUpdateAll();
                    break;

                case Constants.NOTIFY_INSERT:
                    layer.notifyInsert(intent.getLongExtra(FIELD_ID, NOT_FOUND));
                    break;
            }
        }
    }
} public class TrackLayer
        extends Layer
{
    public static final String TABLE_TRACKS      = "tracks";
    public static final String TABLE_TRACKPOINTS = "trackpoints";

    public static final String FIELD_ID      = "_id";
    public static final String FIELD_NAME    = "name";
    public static final String FIELD_START   = "start";
    public static final String FIELD_END     = "end";
    public static final String FIELD_COLOR   = "color";
    public static final String FIELD_VISIBLE = "visible";

    public static final String FIELD_LON       = "lon";
    public static final String FIELD_LAT       = "lat";
    public static final String FIELD_ELE       = "ele";
    public static final String FIELD_FIX       = "fix";
    public static final String FIELD_SAT       = "sat";
    public static final String FIELD_TIMESTAMP = "time";
    public static final String FIELD_SESSION   = "session";
    public static final String FIELD_SENT      = "sent";
    public static final String FIELD_SPEED     = "speed";
    public static final String FIELD_ACCURACY  = "accuracy";

    static final String DB_CREATE_TRACKS      =
            "CREATE TABLE IF NOT EXISTS " + TABLE_TRACKS + " (" +
            FIELD_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
            FIELD_NAME + " TEXT NOT NULL, " +
            FIELD_START + " INTEGER NOT NULL, " +
            FIELD_END + " INTEGER, " +
            FIELD_COLOR + " TEXT, " +
            FIELD_VISIBLE + " INTEGER NOT NULL);";
    static final String DB_CREATE_TRACKPOINTS =
            "CREATE TABLE IF NOT EXISTS " + TABLE_TRACKPOINTS + " (" +
            FIELD_LON + " REAL NOT NULL, " +
            FIELD_LAT + " REAL NOT NULL, " +
            FIELD_ELE + " REAL, " +
            FIELD_FIX + " TEXT, " +
            FIELD_SAT + " INTEGER, " +
            FIELD_SPEED + " REAL, " +
            FIELD_ACCURACY + " REAL, " +
            FIELD_TIMESTAMP + " INTEGER NOT NULL, " +
            FIELD_SENT + " INTEGER NOT NULL, " +
            FIELD_SESSION + " INTEGER NOT NULL, FOREIGN KEY(" + FIELD_SESSION + ") REFERENCES " +
            TABLE_TRACKS + "(" + FIELD_ID + "));";

    private static final int TYPE_TRACKS       = 1;
    private static final int TYPE_TRACKPOINTS  = 2;
    private static final int TYPE_SINGLE_TRACK = 3;

    private static final int UPDATE = 1;
    private static final int INSERT = 2;
    private static final int DELETE = 3;

    private static String CONTENT_TYPE, CONTENT_TYPE_TRACKPOINTS, CONTENT_ITEM_TYPE;

    protected static int    mColor = Color.LTGRAY;
    protected Cursor mCursor;
    String         mAuthority;
    SQLiteDatabase mSQLiteDatabase;
    private UriMatcher mUriMatcher;
    private Uri        mContentUriTracks, mContentUriTrackpoints;
    private MapContentProviderHelper    mMap;
    private Map<Integer, GeoLineString> mTracks;


    public TrackLayer(
            Context context,
            File path)
    {
        super(context, path);

        if (!(getContext() instanceof IGISApplication)) {
            throw new IllegalArgumentException(
                    "The context should be the instance of IGISApplication");
        }

        IGISApplication app = (IGISApplication) getContext();
        mMap = (MapContentProviderHelper) MapBase.getInstance();
        mAuthority = app.getAuthority();

        if (mMap == null) {
            throw new IllegalArgumentException(
                    "Cannot get access to DB (context's MapBase is null)");
        }

        mContentUriTracks = Uri.parse("content://" + mAuthority + "/" + TABLE_TRACKS);
        mContentUriTrackpoints = Uri.parse("content://" + mAuthority + "/" + TABLE_TRACKPOINTS);

        mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        mUriMatcher.addURI(mAuthority, TABLE_TRACKS, TYPE_TRACKS);
        mUriMatcher.addURI(mAuthority, TABLE_TRACKS + "/#", TYPE_SINGLE_TRACK);
        mUriMatcher.addURI(mAuthority, TABLE_TRACKPOINTS, TYPE_TRACKPOINTS);

        if (null == CONTENT_TYPE) {
            CONTENT_TYPE = "vnd.android.cursor.dir/vnd." + mAuthority + "." + TABLE_TRACKS;
        }
        if (null == CONTENT_TYPE_TRACKPOINTS) {
            CONTENT_TYPE_TRACKPOINTS =
                    "vnd.android.cursor.dir/vnd." + mAuthority + "." + TABLE_TRACKPOINTS;
        }
        if (null == CONTENT_ITEM_TYPE) {
            CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd." + mAuthority + "." + TABLE_TRACKS;
        }

        initDB();

        mLayerType = Constants.LAYERTYPE_TRACKS;
        mRenderer = new TrackRenderer(this);
        mTracks = new HashMap<>();
    }


    @Override
    public String getName()
    {
        return mContext.getString(R.string.tracks);
    }


    public Map<Integer, GeoLineString> getTracks()
    {
        if (mTracks.size() == 0) {
            reloadTracks(INSERT);
        }

        return mTracks;
    }


    public void reloadTracks(int mode)
    {
        String[] proj = new String[] {FIELD_ID};
        String selection =
                FIELD_VISIBLE + " = 1 AND " + FIELD_END + " IS NOT NULL AND " + FIELD_END +
                " != ''";

        mCursor =
                mContext.getContentResolver().query(mContentUriTracks, proj, selection, null, null);

        if (null == mCursor) {
            return;
        }

        List<Integer> trackIds = new ArrayList<>();

        if (mCursor.moveToFirst()) {
            do {
                int trackId = mCursor.getInt(mCursor.getColumnIndex(TrackLayer.FIELD_ID));
                trackIds.add(trackId);
            } while (mCursor.moveToNext());

        }

        mCursor.close();

        switch (mode) {
            case UPDATE:
                Iterator itUpdate = mTracks.keySet().iterator();
                while (itUpdate.hasNext()) {
                    Integer entry = (Integer) itUpdate.next();
                    if (trackIds.contains(entry)) {
                        loadTrack(entry);
                    } else {
                        itUpdate.remove();
                    }
                }

                for (int key : trackIds) {
                    if (!mTracks.keySet().contains(key)) {
                        loadTrack(key);
                    }
                }
                break;
            case INSERT:
                for (int key : trackIds) {
                    if (!mTracks.keySet().contains(key)) {
                        loadTrack(key);
                    }
                }
                break;
            case DELETE:
                Iterator itDelete = mTracks.keySet().iterator();
                while (itDelete.hasNext()) {
                    Integer entry = (Integer) itDelete.next();
                    if (!trackIds.contains(entry)) {
                        itDelete.remove();
                    }
                }
                break;
        }
    }


    private void loadTrack(int trackId)
    {
        Cursor track = getTrack(trackId);

        if (track == null || !track.moveToFirst()) {
            return;
        }

        float x0 = track.getFloat(track.getColumnIndex(TrackLayer.FIELD_LON)),
                y0 = track.getFloat(track.getColumnIndex(TrackLayer.FIELD_LAT));

        GeoLineString trackLine = new GeoLineString();
        trackLine.setCRS(GeoConstants.CRS_WEB_MERCATOR);
        trackLine.add(new GeoPoint(x0, y0));

        while (track.moveToNext()) {
            x0 = track.getFloat(track.getColumnIndex(TrackLayer.FIELD_LON));
            y0 = track.getFloat(track.getColumnIndex(TrackLayer.FIELD_LAT));
            trackLine.add(new GeoPoint(x0, y0));
        }

        mTracks.put(trackId, trackLine);
    }


    private Cursor getTrack(int id)
    {
        if (mCursor == null) {
            throw new RuntimeException("Tracks' cursor is null");
        }

        String[] proj = new String[] {FIELD_LON, FIELD_LAT};

        return mContext.getContentResolver().query(
                Uri.withAppendedPath(mContentUriTracks, id + ""), proj, null, null, null);
    }


    public static String getSelection(int size) {
        return TrackLayer.FIELD_ID + " IN (" + MapUtil.makePlaceholders(size) + ")";
    }

    public int getColor(long id) {
        return getColor(mContext, mContentUriTracks, id);
    }

    public static int getColor(Context context, Uri tracksUri, long id)
    {
        String selection = FIELD_ID + " = ?";
        Cursor cursor = context.getContentResolver().query(tracksUri, new String[] {FIELD_COLOR}, selection, new String[]{id + ""}, null);

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

        int color = mColor;
        if (cursor.moveToFirst()) {
            if (!cursor.isNull(0))
                color = cursor.getInt(0);

            cursor.close();
        }

        return color;
    }


//    @Override
//    protected void notifyLayerChanged()
//    {
//        super.notifyLayerChanged();
//        mMap.onLayerChanged(this);
//    }


    private void initDB()
    {
        mSQLiteDatabase = mMap.getDatabase(false);

//        mSQLiteDatabase.execSQL("DROP TABLE IF EXISTS TRACKPOINTS;");
//        mSQLiteDatabase.execSQL("DROP TABLE IF EXISTS TRACKS;");

        mSQLiteDatabase.execSQL(DB_CREATE_TRACKS);
        mSQLiteDatabase.execSQL(DB_CREATE_TRACKPOINTS);
    }


    public Cursor query(
            Uri uri,
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder,
            String limit) throws SQLException, IllegalArgumentException
    {
        mSQLiteDatabase = mMap.getDatabase(true);
        Cursor cursor;

        switch (mUriMatcher.match(uri)) {
            case TYPE_TRACKS:
                cursor = mSQLiteDatabase.query(
                        TABLE_TRACKS, projection, selection, selectionArgs, null, null, sortOrder, limit);
                cursor.setNotificationUri(getContext().getContentResolver(), mContentUriTracks);
                return cursor;
            case TYPE_TRACKPOINTS:
                cursor = mSQLiteDatabase.query(
                        TABLE_TRACKPOINTS, projection, selection, selectionArgs, null, null,
                        sortOrder, limit);

                cursor.setNotificationUri(
                        getContext().getContentResolver(), mContentUriTrackpoints);
                return cursor;
            case TYPE_SINGLE_TRACK:
                String id = uri.getLastPathSegment();

                if (TextUtils.isEmpty(selection)) {
                    selection = FIELD_SESSION + " = " + id;
                } else {
                    selection = selection + " AND " + FIELD_SESSION + " = " + id;
                }

                cursor = mSQLiteDatabase.query(
                        TABLE_TRACKPOINTS, projection, selection, selectionArgs, null, null,
                        sortOrder, limit);
                return cursor;
            default:
                throw new IllegalArgumentException("Wrong tracks URI: " + uri);
        }
    }


    public Uri insert(
            Uri uri,
            ContentValues values)
    {
        mSQLiteDatabase = mMap.getDatabase(false);
        long id;
        Uri inserted;

        switch (mUriMatcher.match(uri)) {
            case TYPE_SINGLE_TRACK:
                values.remove(FIELD_ID);
            case TYPE_TRACKS:
                id = mSQLiteDatabase.insert(TABLE_TRACKS, null, values);
                inserted = ContentUris.withAppendedId(mContentUriTracks, id);
                break;
            case TYPE_TRACKPOINTS:
                id = mSQLiteDatabase.insert(TABLE_TRACKPOINTS, null, values);
                inserted = ContentUris.withAppendedId(mContentUriTrackpoints, id);
                break;
            default:
                throw new IllegalArgumentException("Wrong tracks URI: " + uri);
        }

        if (id != NOT_FOUND) {
//            notifyLayerChanged();
            reloadTracks(INSERT);
            getContext().getContentResolver().notifyChange(inserted, null);
        }

        return inserted;
    }


    public int delete(
            Uri uri,
            String selection,
            String[] selectionArgs)
    {
        mSQLiteDatabase = mMap.getDatabase(false);

        switch (mUriMatcher.match(uri)) {
            case TYPE_TRACKS:
                if (selection != null) {
                    String trackpointsSel = selection.replace(FIELD_ID, FIELD_SESSION);
                    mSQLiteDatabase.delete(TABLE_TRACKPOINTS, trackpointsSel, selectionArgs);
                }
                break;
            case TYPE_SINGLE_TRACK:
            case TYPE_TRACKPOINTS:
                throw new IllegalArgumentException(
                        "Only multiple tracks deletion implemented (WHERE _id IN (?,...,?))");
            default:
                throw new IllegalArgumentException("Wrong tracks URI: " + uri);
        }

        int deleted = mSQLiteDatabase.delete(TABLE_TRACKS, selection, selectionArgs);

        if (deleted > 0) {
//            notifyLayerChanged();
            reloadTracks(DELETE);
            getContext().getContentResolver().notifyChange(uri, null);
        }

        return deleted;
    }


    public int update(
            Uri uri,
            ContentValues values,
            String selection,
            String[] selectionArgs)
    {
        mSQLiteDatabase = mMap.getDatabase(false);
        int updated;
        String table = TABLE_TRACKS;

        switch (mUriMatcher.match(uri)) {
            case TYPE_SINGLE_TRACK:
                String id = uri.getLastPathSegment();

                if (TextUtils.isEmpty(selection)) {
                    selection = FIELD_ID + " = " + id;
                } else {
                    selection = selection + " AND " + FIELD_ID + " = " + id;
                }
            case TYPE_TRACKS:
                mMap.onLayerChanged(this);
//                notifyLayerChanged();
                break;
            case TYPE_TRACKPOINTS:
                table = TABLE_TRACKPOINTS;
                break;
//                throw new IllegalArgumentException("Trackpoints can't be updated");
            default:
                throw new IllegalArgumentException("Wrong tracks URI: " + uri);
        }

        updated = mSQLiteDatabase.update(table, values, selection, selectionArgs);

        if (updated > 0) {
            reloadTracks(UPDATE);
            getContext().getContentResolver().notifyChange(uri, null);
        }

        return updated;
    }


    public String getType(Uri uri)
    {
        switch (mUriMatcher.match(uri)) {
            case TYPE_TRACKS:
                return CONTENT_TYPE;
            case TYPE_SINGLE_TRACK:
                return CONTENT_ITEM_TYPE;
            case TYPE_TRACKPOINTS:
                return CONTENT_TYPE_TRACKPOINTS;
        }

        return null;
    }

    public void sync() {

    }
} 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 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 interface SettingsConstants
{
    String KEY_PREF_MAP                     = "map";
    String KEY_PREF_MAP_PATH                = "map_path";
    String KEY_PREF_LAST_SYNC_TIMESTAMP     = "last_sync_timestamp";
    String KEY_PREF_LOCATION_SOURCE         = "location_source";
    String KEY_PREF_LOCATION_MIN_TIME       = "location_min_time";
    String KEY_PREF_LOCATION_MIN_DISTANCE   = "location_min_distance";
    String KEY_PREF_LOCATION_ACCURATE_COUNT = "accurate_max_count";
    String KEY_PREF_LOCATION_ACCURATE_TIME  = "accurate_max_time";
    String KEY_PREF_LOCATION_ACCURATE_CE    = "accurate_ce";
    String KEY_PREF_TRACKS_MIN_TIME         = "tracks_min_time";
    String KEY_PREF_TRACKS_MIN_DISTANCE     = "tracks_min_distance";
    String KEY_PREF_TRACKS_SOURCE           = "tracks_location_source";
    String KEY_PREF_TRACK_RESTORE           = "track_restore";
    String KEY_PREF_TRACK_SEND              = "track_send";
    String KEY_PREF_UNITS                   = "preferred_units";
}
import android.accounts.Account;
import android.accounts.AccountManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.hypertrack.hyperlog.HyperLog;
import com.nextgis.maplib.R;
import com.nextgis.maplib.api.IGISApplication;
import com.nextgis.maplib.api.ILayer;
import com.nextgis.maplib.api.INGWLayer;
import com.nextgis.maplib.map.LayerGroup;
import com.nextgis.maplib.map.MapBase;
import com.nextgis.maplib.map.MapContentProviderHelper;
import com.nextgis.maplib.map.TrackLayer;
import com.nextgis.maplib.util.Constants;
import com.nextgis.maplib.util.NGWUtil;
import com.nextgis.maplib.util.SettingsConstants;
import java.util.HashMap;
import static android.content.Context.MODE_MULTI_PROCESS;
import static com.nextgis.maplib.util.Constants.TAG;
/*
 * 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) 2015-2017, 2019-2021 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.datasource.ngw;





/* useful links
https://udinic.wordpress.com/2013/07/24/write-your-own-android-sync-adapter/#more-507
http://www.fussylogic.co.uk/blog/?p=1031
http://www.fussylogic.co.uk/blog/?p=1035
http://www.fussylogic.co.uk/blog/?p=1037
http://developer.android.com/training/sync-adapters/creating-sync-adapter.html
https://github.com/elegion/ghsync
http://habrahabr.ru/company/e-Legion/blog/206210/
http://habrahabr.ru/company/e-Legion/blog/216857/
http://stackoverflow.com/questions/5486228/how-do-we-control-an-android-sync-adapter-preference
https://books.google.ru/books?id=SXlMAQAAQBAJ&pg=PA158&lpg=PA158&dq=android:syncAdapterSettingsAction&source=bl&ots=T832S7VvKb&sig=vgNNDHfwyMzvINeHfdfDhu9tREs&hl=ru&sa=X&ei=YviqVIPMF9DgaPOUgOgP&ved=0CFUQ6AEwBw#v=onepage&q=android%3AsyncAdapterSettingsAction&f=false
*/


public class SyncAdapter
        extends AbstractThreadedSyncAdapter
{
    public static final String SYNC_START    = "com.nextgis.maplib.sync_start";
    public static final String SYNC_FINISH   = "com.nextgis.maplib.sync_finish";
    public static final String SYNC_CANCELED = "com.nextgis.maplib.sync_canceled";
    public static final String SYNC_CHANGES  = "com.nextgis.maplib.sync_changes";

    public static final String EXCEPTION = "exception";
    protected String mError;

    private HashMap<String, Pair<Integer, Integer>> mVersions;

    public SyncAdapter(
            Context context,
            boolean autoInitialize)
    {
        super(context, autoInitialize);
    }


    public SyncAdapter(
            Context context,
            boolean autoInitialize,
            boolean allowParallelSyncs)
    {
        super(context, autoInitialize, allowParallelSyncs);
    }


    /**
     * Warning! When you stop the sync service by ContentResolver.cancelSync() then onPerformSync
     * stops after end of syncing of current NGWVectorLayer. The data structure of the current
     * NGWVectorLayer will be saved.
     * <p/>
     * <b>Description copied from class:</b> AbstractThreadedSyncAdapter Perform a sync for this
     * account. SyncAdapter-specific parameters may be specified in extras, which is guaranteed to
     * not be null. Invocations of this method are guaranteed to be serialized.
     */
    @Override
    public void onPerformSync(
            Account account,
            Bundle bundle,
            String authority,
            ContentProviderClient contentProviderClient,
            SyncResult syncResult)
    {
        Log.d(TAG, "onPerformSync");
        HyperLog.v(Constants.TAG, "SyncAdapter: onPerformSync");

        MapContentProviderHelper mapContentProviderHelper =(MapContentProviderHelper) MapBase.getInstance();
        getContext().sendBroadcast(new Intent(SYNC_START));

        mVersions = new HashMap<>();
        HyperLog.v(Constants.TAG, "SyncAdapter: mapContentProviderHelper is " + mapContentProviderHelper);
        if (null != mapContentProviderHelper) {
            // FIXME Temporary fix till 3.0
//            mapContentProviderHelper.load(); // reload map for deleted/added layers
            sync(mapContentProviderHelper, authority, syncResult);
        }

        if (isCanceled()) {
            Log.d(Constants.TAG, "onPerformSync - SYNC_CANCELED is sent");
            HyperLog.v(Constants.TAG, "SyncAdapter: SYNC_CANCELED is sent");
            getContext().sendBroadcast(new Intent(SYNC_CANCELED));
            return;
        }

        final String accountNameHash = "_" + account.name.hashCode();
        SharedPreferences settings = getContext().getSharedPreferences(Constants.PREFERENCES, MODE_MULTI_PROCESS);
        SharedPreferences.Editor editor = settings.edit();
        editor.putLong(SettingsConstants.KEY_PREF_LAST_SYNC_TIMESTAMP + accountNameHash, System.currentTimeMillis());
        editor.putLong(SettingsConstants.KEY_PREF_LAST_SYNC_TIMESTAMP, System.currentTimeMillis());
        editor.apply();

        mError = "";
        if (syncResult.stats.numIoExceptions > 0)
            mError += getContext().getString(R.string.sync_error_io);
        if (syncResult.stats.numParseExceptions > 0) {
            if (mError.length() > 0)
                mError += "\r\n";
            mError += getContext().getString(R.string.sync_error_parse);
        }
        if (syncResult.stats.numAuthExceptions > 0) {
            if (mError.length() > 0)
                mError += "\r\n";
            mError += getContext().getString(R.string.error_auth);
        }
        if (syncResult.stats.numConflictDetectedExceptions > 0) {
            if (mError.length() > 0)
                mError += "\r\n";
            mError += getContext().getString(R.string.sync_error_conflict);
        }
        if (syncResult.stats.numInserts > 0) {
            if (mError.length() > 0)
                mError += "\r\n";
            mError += getContext().getString(R.string.sync_error_insert);
        }
        if (syncResult.stats.numUpdates > 0) {
            if (mError.length() > 0)
                mError += "\r\n";
            mError += getContext().getString(R.string.sync_error_change);
        }
        if (syncResult.stats.numDeletes > 0) {
            if (mError.length() > 0)
                mError += "\r\n";
            mError += getContext().getString(R.string.sync_error_delete);
        }
        if (syncResult.stats.numEntries > 0) {
            if (mError.length() > 0)
                mError += "\r\n";
            mError += getContext().getString(R.string.sync_error_server);
        }
        if (syncResult.stats.numSkippedEntries > 0) {
            if (mError.length() > 0)
                mError += "\r\n";
            mError += getContext().getString(R.string.sync_error_oom);
        }

        Intent finish = new Intent(SYNC_FINISH);
        if (!TextUtils.isEmpty(mError))
            finish.putExtra(EXCEPTION, mError);
        HyperLog.v(Constants.TAG, "SyncAdapter: SYNC_FINISH is sent / mError is " + mError);
        getContext().sendBroadcast(finish);
    }


    protected void sync(
            LayerGroup layerGroup,
            String authority,
            SyncResult syncResult)
    {
        HyperLog.v(Constants.TAG, "SyncAdapter: total layers for sync in " + layerGroup + " is " + layerGroup.getLayerCount());
        for (int i = 0; i < layerGroup.getLayerCount(); i++) {
            if (isCanceled()) {
                return;
            }
            ILayer layer = layerGroup.getLayer(i);
            if (layer instanceof LayerGroup) {
                HyperLog.v(Constants.TAG, "SyncAdapter: " + layer + " is a layer group");
                sync((LayerGroup) layer, authority, syncResult);
            } else if (layer instanceof INGWLayer) {