@Override
        public void run() {
            String logParams = "";
            try {
                //quick abort if nothing is requested
                if (this.loader.dirtyTime < 0) {
                    return;
                }

                final Viewport viewport;
                final GeocacheFilter filter;
                synchronized (loader) {
                    //check whether current request was already served with last request
                    viewport = this.loader.viewport;
                    filter = this.loader.filter;
                    logParams = "(vp=" + viewport + ",f=" + filter + ")";

                    //quick exit on invalid viewport
                    if (!Viewport.isValid(viewport)) {
                        Log.iForce(LOGPRAEFIX + "INVALID VIEWPORT " + logParams);
                        setState(LoadState.STOPPED);
                        this.loader.dirtyTime = -1;
                        return;
                    }

                    final boolean cacheIsValidForFilter = GeocacheFilter.filtersSame(cachedResultFilter, filter) && (System.currentTimeMillis() - cachedResultTs) < CACHE_EXPIRY;

                    //quick exit on unconditional cache hit
                    if (cacheIsValidForFilter && cachedResultAvailableViewport != null && cachedResultAvailableViewport.includes(viewport)) {
                        Log.iForce(LOGPRAEFIX + "CACHE HIT " + logParams);
                        setState(LoadState.STOPPED);
                        this.loader.dirtyTime = -1;
                        return;
                    }

                    //quick exit on previous PARTIAL or ERROR result if viewport hasn't moved much
                    if (cacheIsValidForFilter && !cachedResultWasComplete && cachedResultCenter != null) {
                        final float distanceCachedCall = viewport.getCenter().distanceTo(cachedResultCenter);
                        final float distanceCenterCorner = viewport.getCenter().distanceTo(viewport.bottomLeft);
                        if (distanceCachedCall <= distanceCenterCorner * 0.2) {
                            Log.iForce(LOGPRAEFIX + "NO RELOAD AFTER NONCOMPLETE RESULT AND TOO CLOSE TO PREVIOUS " + logParams);
                            setState(LoadState.STOPPED);
                            this.loader.dirtyTime = -1;
                            return;
                        }
                    }


                    //if request is real check if its time to really request
                    final long requestAge = System.currentTimeMillis() - this.loader.dirtyTime;
                    if (requestAge < PROCESS_DELAY) {
                        setState(LoadState.REQUESTED);
                        return;
                    }

                    //if we come here we HAVE to do an online request. Reset dirty state
                    this.loader.dirtyTime = -1;
                }

                Log.iForce(LOGPRAEFIX + "START" + logParams);
                setState(LoadState.RUNNING);


                final Map<String, ConnectorState> stateData = Collections.synchronizedMap(new HashMap<>());
                // DO ONLINE REQUEST retrieving live caches for x times the requested size (to fill up cache)
                final Viewport retrievalViewport = viewport.resize(3.0);
                final long startTs = System.currentTimeMillis();
                final boolean[] isOverallComplete = new boolean[] { true };
                ConnectorFactory.searchByViewport(retrievalViewport, filter, (c, sr) -> {
                    final long duration = System.currentTimeMillis() - startTs;
                    //handle and send cache results for one connector
                    final int countOriginal = sr.getCount();
                    final Set<Geocache> result = sr.getCachesFromSearchResult(LoadFlags.LOAD_CACHE_OR_DB);
                    final int countInDb = result.size();
                    if (filter != null) {
                        filter.filterList(result);
                    }
                    final int countFiltered = result.size();
                    onResult.accept(result);

                    //collect caching information
                    final boolean isPartial = !sr.getPartialConnectors().isEmpty();
                    final StatusCode errorCode = sr.getError();
                    final Viewport connectorViewport;
                    if (errorCode != StatusCode.NO_ERROR) { // || isPartial && !connector supports centered-results!!!
                        connectorViewport = null;
                        isOverallComplete[0] = false;
                    } else if (isPartial) {
                       connectorViewport = Viewport.containing(result);
                        isOverallComplete[0] = false;
                    } else {
                        connectorViewport = retrievalViewport;
                    }
                    //set state data
                    stateData.put(c.getName(), new ConnectorState(c.getName(), errorCode, connectorViewport, countOriginal, countInDb, countFiltered, duration, null));
                });

                //adjust result cache. cachedResultViewportOverall will be null on intersect if one connector failed
                this.cachedResultAvailableViewport = Viewport.intersect(stateData.values(), sd -> sd.viewport);
                this.cachedResultFilter = filter;
                this.cachedResultTs = System.currentTimeMillis();
                this.cachedResultCenter = viewport.getCenter();
                this.cachedResultWasComplete = isOverallComplete[0];

                //do status update
                setState(LoadState.STOPPED, stateData);

                Log.iForce(LOGPRAEFIX + "END  " + logParams + ": cachedViewport:" + this.cachedResultAvailableViewport + ", state: " + stateData);

            } catch (Exception e) {
                Log.e(LOGPRAEFIX + "UNEXPECTED ERROR" + logParams, e);
                setState(LoadState.STOPPED, Collections.singletonMap("Overall",
                    new ConnectorState("Overall", StatusCode.UNKNOWN_ERROR, null, 0, 0, 0, -1, "Exception: " + e.getMessage())));
            }
            //if we have just done an online request, ensure that there's a grace period for the next
            synchronized (loader) {
                if (this.loader.dirtyTime > 0) {
                    this.loader.dirtyTime = System.currentTimeMillis();
                }
            }
        }
accepts requests to load geocaches online but allows configuring of a maximum update rate
main/src/main/java/cgeo/geocaching/unifiedmap/LiveMapGeocacheLoader.java