Dobry sposób na uzyskanie lokalizacji użytkownika w Androidzie


211

Problem:

Uzyskiwanie bieżącej lokalizacji użytkownika w ramach progu JAK NAJSZYBCIEJ, a jednocześnie oszczędzanie baterii.

Dlaczego problem jest problemem:

Po pierwsze, Android ma dwóch dostawców; sieć i GPS. Czasami sieć jest lepsza, a czasem GPS jest lepszy.

Przez „lepszy” rozumiem stosunek prędkości do dokładności.
Jestem gotów poświęcić kilka metrów dokładności, jeśli mogę uzyskać lokalizację niemal natychmiast i bez włączania GPS.

Po drugie, jeśli poprosisz o aktualizacje w celu zmiany lokalizacji, nic nie zostanie wysłane, jeśli bieżąca lokalizacja jest stabilna.

Google ma przykład określania „najlepszej” lokalizacji tutaj: http://developer.android.com/guide/topics/location/obtaining-user-location.html#BestEstimate
Ale myślę, że nie jest tak blisko, jak powinien /możliwe.

Jestem trochę zdezorientowany, dlaczego Google nie ma znormalizowanego interfejsu API dla lokalizacji, programista nie powinien dbać o to, skąd pochodzi lokalizacja, powinieneś po prostu określić, co chcesz, a telefon powinien wybrać dla Ciebie.

W czym potrzebuję pomocy:

Muszę znaleźć dobry sposób na określenie „najlepszej” lokalizacji, może za pomocą heurystyki lub innej biblioteki innej firmy.

Nie oznacza to, że należy wybrać najlepszego dostawcę!
Prawdopodobnie skorzystam ze wszystkich dostawców i wybiorę najlepszych z nich.

Tło aplikacji:

Aplikacja będzie zbierać lokalizację użytkownika w ustalonych odstępach czasu (powiedzmy co około 10 minut) i wysyła ją na serwer.
Aplikacja powinna oszczędzać jak najwięcej baterii, a lokalizacja powinna mieć dokładność X (50-100?).

Celem jest później możliwość wykreślenia ścieżki użytkownika w ciągu dnia na mapie, więc potrzebuję do tego wystarczającej dokładności.

Różne:

Jak myślisz, jakie są rozsądne wartości pożądanych i akceptowanych dokładności?
Używałem 100 m zgodnie z akceptacją i 30 m zgodnie z życzeniem. Czy to zbyt wiele pytań?
Chciałbym móc później nakreślić ścieżkę użytkownika na mapie.
Czy 100m dla pożądanego i 500m dla zaakceptowanego jest lepsze?

Poza tym, teraz mam włączony GPS na maksymalnie 60 sekund na aktualizację lokalizacji. Czy jest to zbyt krótkie, aby uzyskać lokalizację, jeśli przebywasz w pomieszczeniu z dokładnością do 200 m?


To jest mój obecny kod, każda opinia jest mile widziana (poza brakiem sprawdzania błędów, którym jest TODO):

protected void runTask() {
    final LocationManager locationManager = (LocationManager) context
            .getSystemService(Context.LOCATION_SERVICE);
    updateBestLocation(locationManager
            .getLastKnownLocation(LocationManager.GPS_PROVIDER));
    updateBestLocation(locationManager
            .getLastKnownLocation(LocationManager.NETWORK_PROVIDER));
    if (getLocationQuality(bestLocation) != LocationQuality.GOOD) {
        Looper.prepare();
        setLooper(Looper.myLooper());
        // Define a listener that responds to location updates
        LocationListener locationListener = new LocationListener() {

            public void onLocationChanged(Location location) {
                updateBestLocation(location);
                if (getLocationQuality(bestLocation) != LocationQuality.GOOD)
                    return;
                // We're done
                Looper l = getLooper();
                if (l != null) l.quit();
            }

            public void onProviderEnabled(String provider) {}

            public void onProviderDisabled(String provider) {}

            public void onStatusChanged(String provider, int status,
                    Bundle extras) {
                // TODO Auto-generated method stub
                Log.i("LocationCollector", "Fail");
                Looper l = getLooper();
                if (l != null) l.quit();
            }
        };
        // Register the listener with the Location Manager to receive
        // location updates
        locationManager.requestLocationUpdates(
                LocationManager.GPS_PROVIDER, 1000, 1, locationListener,
                Looper.myLooper());
        locationManager.requestLocationUpdates(
                LocationManager.NETWORK_PROVIDER, 1000, 1,
                locationListener, Looper.myLooper());
        Timer t = new Timer();
        t.schedule(new TimerTask() {

            @Override
            public void run() {
                Looper l = getLooper();
                if (l != null) l.quit();
                // Log.i("LocationCollector",
                // "Stopping collector due to timeout");
            }
        }, MAX_POLLING_TIME);
        Looper.loop();
        t.cancel();
        locationManager.removeUpdates(locationListener);
        setLooper(null);
    }
    if (getLocationQuality(bestLocation) != LocationQuality.BAD) 
        sendUpdate(locationToString(bestLocation));
    else Log.w("LocationCollector", "Failed to get a location");
}

private enum LocationQuality {
    BAD, ACCEPTED, GOOD;

    public String toString() {
        if (this == GOOD) return "Good";
        else if (this == ACCEPTED) return "Accepted";
        else return "Bad";
    }
}

private LocationQuality getLocationQuality(Location location) {
    if (location == null) return LocationQuality.BAD;
    if (!location.hasAccuracy()) return LocationQuality.BAD;
    long currentTime = System.currentTimeMillis();
    if (currentTime - location.getTime() < MAX_AGE
            && location.getAccuracy() <= GOOD_ACCURACY)
        return LocationQuality.GOOD;
    if (location.getAccuracy() <= ACCEPTED_ACCURACY)
        return LocationQuality.ACCEPTED;
    return LocationQuality.BAD;
}

private synchronized void updateBestLocation(Location location) {
    bestLocation = getBestLocation(location, bestLocation);
}

// Pretty much an unmodified version of googles example
protected Location getBestLocation(Location location,
        Location currentBestLocation) {
    if (currentBestLocation == null) {
        // A new location is always better than no location
        return location;
    }
    if (location == null) return currentBestLocation;
    // Check whether the new location fix is newer or older
    long timeDelta = location.getTime() - currentBestLocation.getTime();
    boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
    boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
    boolean isNewer = timeDelta > 0;
    // If it's been more than two minutes since the current location, use
    // the new location
    // because the user has likely moved
    if (isSignificantlyNewer) {
        return location;
        // If the new location is more than two minutes older, it must be
        // worse
    } else if (isSignificantlyOlder) {
        return currentBestLocation;
    }
    // Check whether the new location fix is more or less accurate
    int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation
            .getAccuracy());
    boolean isLessAccurate = accuracyDelta > 0;
    boolean isMoreAccurate = accuracyDelta < 0;
    boolean isSignificantlyLessAccurate = accuracyDelta > 200;
    // Check if the old and new location are from the same provider
    boolean isFromSameProvider = isSameProvider(location.getProvider(),
            currentBestLocation.getProvider());
    // Determine location quality using a combination of timeliness and
    // accuracy
    if (isMoreAccurate) {
        return location;
    } else if (isNewer && !isLessAccurate) {
        return location;
    } else if (isNewer && !isSignificantlyLessAccurate
            && isFromSameProvider) {
        return location;
    }
    return bestLocation;
}

/** Checks whether two providers are the same */
private boolean isSameProvider(String provider1, String provider2) {
    if (provider1 == null) {
        return provider2 == null;
    }
    return provider1.equals(provider2);
}

7
Chiming spóźnia się naprawdę, ale „dostawca bezpiecznej lokalizacji”, który został niedawno ogłoszony na IO 2013, wygląda na to, że spełnia wiele twoich potrzeb - developer.android.com/google/play-services/location.html
Matt

ostatnia linia getBestLocation () nie powinna być: return currentBestLocation; zamiast zwracać bestLocation ;?
Gavriel,

Odpowiedzi:


164

Wygląda na to, że kodujemy tę samą aplikację ;-)
Oto moja obecna implementacja. Wciąż jestem w fazie testów beta mojej aplikacji do przesyłania GPS, więc może być wiele możliwych ulepszeń. ale wydaje się, że do tej pory działało całkiem dobrze.

/**
 * try to get the 'best' location selected from all providers
 */
private Location getBestLocation() {
    Location gpslocation = getLocationByProvider(LocationManager.GPS_PROVIDER);
    Location networkLocation =
            getLocationByProvider(LocationManager.NETWORK_PROVIDER);
    // if we have only one location available, the choice is easy
    if (gpslocation == null) {
        Log.d(TAG, "No GPS Location available.");
        return networkLocation;
    }
    if (networkLocation == null) {
        Log.d(TAG, "No Network Location available");
        return gpslocation;
    }
    // a locationupdate is considered 'old' if its older than the configured
    // update interval. this means, we didn't get a
    // update from this provider since the last check
    long old = System.currentTimeMillis() - getGPSCheckMilliSecsFromPrefs();
    boolean gpsIsOld = (gpslocation.getTime() < old);
    boolean networkIsOld = (networkLocation.getTime() < old);
    // gps is current and available, gps is better than network
    if (!gpsIsOld) {
        Log.d(TAG, "Returning current GPS Location");
        return gpslocation;
    }
    // gps is old, we can't trust it. use network location
    if (!networkIsOld) {
        Log.d(TAG, "GPS is old, Network is current, returning network");
        return networkLocation;
    }
    // both are old return the newer of those two
    if (gpslocation.getTime() > networkLocation.getTime()) {
        Log.d(TAG, "Both are old, returning gps(newer)");
        return gpslocation;
    } else {
        Log.d(TAG, "Both are old, returning network(newer)");
        return networkLocation;
    }
}

/**
 * get the last known location from a specific provider (network/gps)
 */
private Location getLocationByProvider(String provider) {
    Location location = null;
    if (!isProviderSupported(provider)) {
        return null;
    }
    LocationManager locationManager = (LocationManager) getApplicationContext()
            .getSystemService(Context.LOCATION_SERVICE);
    try {
        if (locationManager.isProviderEnabled(provider)) {
            location = locationManager.getLastKnownLocation(provider);
        }
    } catch (IllegalArgumentException e) {
        Log.d(TAG, "Cannot acces Provider " + provider);
    }
    return location;
}

Edycja: tutaj jest część, która żąda okresowych aktualizacji od dostawców lokalizacji:

public void startRecording() {
    gpsTimer.cancel();
    gpsTimer = new Timer();
    long checkInterval = getGPSCheckMilliSecsFromPrefs();
    long minDistance = getMinDistanceFromPrefs();
    // receive updates
    LocationManager locationManager = (LocationManager) getApplicationContext()
            .getSystemService(Context.LOCATION_SERVICE);
    for (String s : locationManager.getAllProviders()) {
        locationManager.requestLocationUpdates(s, checkInterval,
                minDistance, new LocationListener() {

                    @Override
                    public void onStatusChanged(String provider,
                            int status, Bundle extras) {}

                    @Override
                    public void onProviderEnabled(String provider) {}

                    @Override
                    public void onProviderDisabled(String provider) {}

                    @Override
                    public void onLocationChanged(Location location) {
                        // if this is a gps location, we can use it
                        if (location.getProvider().equals(
                                LocationManager.GPS_PROVIDER)) {
                            doLocationUpdate(location, true);
                        }
                    }
                });
        // //Toast.makeText(this, "GPS Service STARTED",
        // Toast.LENGTH_LONG).show();
        gps_recorder_running = true;
    }
    // start the gps receiver thread
    gpsTimer.scheduleAtFixedRate(new TimerTask() {

        @Override
        public void run() {
            Location location = getBestLocation();
            doLocationUpdate(location, false);
        }
    }, 0, checkInterval);
}

public void doLocationUpdate(Location l, boolean force) {
    long minDistance = getMinDistanceFromPrefs();
    Log.d(TAG, "update received:" + l);
    if (l == null) {
        Log.d(TAG, "Empty location");
        if (force)
            Toast.makeText(this, "Current location not available",
                    Toast.LENGTH_SHORT).show();
        return;
    }
    if (lastLocation != null) {
        float distance = l.distanceTo(lastLocation);
        Log.d(TAG, "Distance to last: " + distance);
        if (l.distanceTo(lastLocation) < minDistance && !force) {
            Log.d(TAG, "Position didn't change");
            return;
        }
        if (l.getAccuracy() >= lastLocation.getAccuracy()
                && l.distanceTo(lastLocation) < l.getAccuracy() && !force) {
            Log.d(TAG,
                    "Accuracy got worse and we are still "
                      + "within the accuracy range.. Not updating");
            return;
        }
        if (l.getTime() <= lastprovidertimestamp && !force) {
            Log.d(TAG, "Timestamp not never than last");
            return;
        }
    }
    // upload/store your location here
}

Rzeczy do rozważenia:

  • nie żądaj zbyt często aktualizacji GPS, powoduje to wyczerpanie baterii. Obecnie używam 30 minut jako domyślnej dla mojej aplikacji.

  • dodaj opcję „minimalna odległość do ostatniej znanej lokalizacji”. bez tego punkty będą „przeskakiwać”, gdy GPS nie będzie dostępny, a lokalizacja zostanie triangulowana z wież komórkowych. lub możesz sprawdzić, czy nowa lokalizacja jest poza wartością dokładności z ostatniej znanej lokalizacji.


2
W rzeczywistości nigdy nie dostajesz nowej lokalizacji, korzystasz tylko z lokalizacji, które zdarzają się tam z poprzednich aktualizacji. Myślę, że ten kod skorzystałby bardzo, dodając słuchacza, który aktualizuje lokalizację, włączając od czasu do czasu GPS.
Nicklas A.,

2
przepraszam, myślałem, że interesuje Cię tylko ta część, która wybiera najlepszą spośród wszystkich dostępnych lokalizacji. Dodałem powyższy kod, który również tego wymaga. jeśli zostanie odebrana nowa lokalizacja GPS, zostanie ona natychmiast zapisana / przesłana. jeśli otrzymam aktualizację lokalizacji sieci, przechowuję ją w celach informacyjnych i „mam nadzieję”, że otrzymam również aktualizację GPS, dopóki nie nastąpi następne sprawdzenie lokalizacji.
Gryphius,

2
Miałem także metodę stopRecording (), która anulowała timer. W końcu przestawiłem się z timera na ScheduledThreadPoolExecutor, więc stopRecording teraz w zasadzie wywołuje executor.shutdown () i wyrejestrowuje wszystkie nasłuchiwania aktualizacji lokalizacji
Gryphius

1
zgodnie z moim scm, stopRecording wywołuje tylko gpsTimer.cancel () i ustaw gps_recorder_running = false, więc podobnie jak w twoim przypadku, nie było wtedy czyszczenia słuchaczy. obecny kod śledzi wszystkich aktywnych słuchaczy w wektorze, nie miałem tego, kiedy napisałem tę odpowiedź 1,5 roku temu.
Gryphius

1
jest już na githubie , ale nie jestem pewien, czy jest to obecnie najlepszy sposób na robienie GPS. Afaik, wprowadzili wiele ulepszeń do API lokalizacji, odkąd napisałem ten kod.
Gryphius

33

Aby wybrać odpowiedniego dostawcę lokalizacji dla swojej aplikacji, możesz użyć obiektów Criteria :

Criteria myCriteria = new Criteria();
myCriteria.setAccuracy(Criteria.ACCURACY_HIGH);
myCriteria.setPowerRequirement(Criteria.POWER_LOW);
// let Android select the right location provider for you
String myProvider = locationManager.getBestProvider(myCriteria, true); 

// finally require updates at -at least- the desired rate
long minTimeMillis = 600000; // 600,000 milliseconds make 10 minutes
locationManager.requestLocationUpdates(myProvider,minTimeMillis,0,locationListener); 

Zapoznaj się z dokumentacją requestLocationUpdates, aby uzyskać więcej informacji na temat uwzględnienia argumentów:

Częstotliwość powiadomień można kontrolować za pomocą parametrów minTime i minDistance. Jeśli minTime jest większy niż 0, menedżer lokalizacji może potencjalnie odpoczywać przez minTime w milisekundach między aktualizacjami lokalizacji, aby oszczędzać energię. Jeśli minDistance jest większe niż 0, lokalizacja będzie nadawana tylko wtedy, gdy urządzenie porusza się o mierniki minDistance. Aby otrzymywać powiadomienia tak często, jak to możliwe, ustaw oba parametry na 0.

Więcej myśli

  • Możesz monitorować dokładność obiektów Location za pomocą Location.getAccuracy () , która zwraca szacunkową dokładność pozycji w metrach.
  • Criteria.ACCURACY_HIGHkryterium powinno dać błędy poniżej 100m, co nie jest tak dobry jak GPS może być, ale pasuje do Twoich potrzeb.
  • Musisz także monitorować stan swojego dostawcy lokalizacji i przełączyć się na innego dostawcę, jeśli użytkownik stanie się niedostępny lub wyłączony.
  • Pasywny dostawca może być również dobrym miejscem dla tego typu aplikacji: chodzi o to, aby korzystać z aktualizacji lokalizacji, gdy są one wymagane przez inną aplikację i transmisji systemowy.

Zajrzałem do, Criteriaale co jeśli najnowsza lokalizacja sieci jest niesamowita (może wiedzieć przez Wi-Fi) i nie zajmuje czasu ani baterii, aby ją zdobyć (getLastKnown), wówczas kryteria prawdopodobnie zignorują to i zwrócą GPS. Nie mogę uwierzyć, że Google utrudniło to programistom.
Nicklas A.,

Oprócz korzystania z kryteriów można przy każdej aktualizacji lokalizacji przesłanej przez wybranego usługodawcę sprawdzić ostatnią lokalizację dla dostawcy GPS i porównać ją (dokładność i data) z bieżącą lokalizacją. Ale wydaje mi się, że jest to raczej przyjemne niż wymóg z twoich specyfikacji; jeśli czasami osiąga się nieco lepszą dokładność, czy naprawdę przydadzą się użytkownikom?
Stéphane

Właśnie to robię teraz, problem polega na tym, że trudno mi ustalić, czy ostatnia wiedza jest wystarczająco dobra. Mogę również dodać, że nie muszę ograniczać się do jednego dostawcy, im więcej korzystam, tym szybciej mogę uzyskać blokadę.
Nicklas A.

Pamiętaj, że PASSIVE_PROVIDER wymaga interfejsu API poziomu 8 lub wyższego.
Eduardo

@ Stéphane przepraszam za edycję. Nie zajmuj się tym. Twój post jest poprawny. Zrobiłem tę edycję z powodu błędu. Przepraszam. Pozdrowienia.
Gaucho

10

Odpowiadając na dwa pierwsze punkty :

  • GPS zawsze zapewni dokładniejszą lokalizację, jeśli jest włączona i jeśli nie ma wokół niej grubych ścian .

  • Jeśli lokalizacja się nie zmieniła, możesz wywołać getLastKnownLocation (String) i natychmiast pobrać lokalizację.

Stosując alternatywne podejście :

Możesz spróbować użyć identyfikatora komórki lub wszystkich sąsiednich komórek

TelephonyManager mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
GsmCellLocation loc = (GsmCellLocation) mTelephonyManager.getCellLocation(); 
Log.d ("CID", Integer.toString(loc.getCid()));
Log.d ("LAC", Integer.toString(loc.getLac()));
// or 
List<NeighboringCellInfo> list = mTelephonyManager.getNeighboringCellInfo ();
for (NeighboringCellInfo cell : list) {
    Log.d ("CID", Integer.toString(cell.getCid()));
    Log.d ("LAC", Integer.toString(cell.getLac()));
}

Następnie możesz odnieść się do lokalizacji komórki za pomocą kilku otwartych baz danych (np. Http://www.location-api.com/ lub http://opencellid.org/ )


Strategia polegałaby na odczytaniu listy identyfikatorów wież podczas czytania lokalizacji. Następnie w kolejnym zapytaniu (10 minut w aplikacji) przeczytaj je ponownie. Jeśli przynajmniej niektóre wieże są takie same, można z niego bezpiecznie korzystać getLastKnownLocation(String). Jeśli nie, poczekaj na onLocationChanged(). Pozwala to uniknąć potrzeby bazy danych strony trzeciej dla lokalizacji. Możesz także wypróbować to podejście .


Tak, ale problem pojawia się, jeśli ostatnia lokalizacja jest naprawdę zła. Potrzebuję dobrego sposobu na wybranie najlepszej z dwóch lokalizacji.
Nicklas A.,

Możesz przechowywać informacje o wieżach i sprawdzać, czy wieże się zmieniły. Jeśli tak, to poczekaj na nową lokalizację, jeśli nie (lub jeśli tylko niektóre się zmieniły), a następnie użyj jej ponownie. W ten sposób unikniesz porównywania lokalizacji wieży z bazą danych.
Aleadam

Korzystanie z wież wydaje mi się dużą przesadą, ale dobry pomysł.
Nicklas A.

@Nicklas kod nie staje się bardziej skomplikowany. Będziesz jednak potrzebował android.Manifest.permission # ACCESS_COARSE_UPDATES.
Aleadam

Tak, ale nadal muszę korzystać z usługi innej firmy, a także potrzebuję sposobu, aby zdecydować, kiedy użyć informacji o wieży w stosunku do danych o lokalizacji, to po prostu dodaje dodatkową warstwę złożoności.
Nicklas A.

9

To jest moje rozwiązanie, które działa całkiem dobrze:

private Location bestLocation = null;
private Looper looper;
private boolean networkEnabled = false, gpsEnabled = false;

private synchronized void setLooper(Looper looper) {
    this.looper = looper;
}

private synchronized void stopLooper() {
    if (looper == null) return;
    looper.quit();
}

@Override
protected void runTask() {
    final LocationManager locationManager = (LocationManager) service
            .getSystemService(Context.LOCATION_SERVICE);
    final SharedPreferences prefs = getPreferences();
    final int maxPollingTime = Integer.parseInt(prefs.getString(
            POLLING_KEY, "0"));
    final int desiredAccuracy = Integer.parseInt(prefs.getString(
            DESIRED_KEY, "0"));
    final int acceptedAccuracy = Integer.parseInt(prefs.getString(
            ACCEPTED_KEY, "0"));
    final int maxAge = Integer.parseInt(prefs.getString(AGE_KEY, "0"));
    final String whichProvider = prefs.getString(PROVIDER_KEY, "any");
    final boolean canUseGps = whichProvider.equals("gps")
            || whichProvider.equals("any");
    final boolean canUseNetwork = whichProvider.equals("network")
            || whichProvider.equals("any");
    if (canUseNetwork)
        networkEnabled = locationManager
                .isProviderEnabled(LocationManager.NETWORK_PROVIDER);
    if (canUseGps)
        gpsEnabled = locationManager
                .isProviderEnabled(LocationManager.GPS_PROVIDER);
    // If any provider is enabled now and we displayed a notification clear it.
    if (gpsEnabled || networkEnabled) removeErrorNotification();
    if (gpsEnabled)
        updateBestLocation(locationManager
                .getLastKnownLocation(LocationManager.GPS_PROVIDER));
    if (networkEnabled)
        updateBestLocation(locationManager
                .getLastKnownLocation(LocationManager.NETWORK_PROVIDER));
    if (desiredAccuracy == 0
            || getLocationQuality(desiredAccuracy, acceptedAccuracy,
                    maxAge, bestLocation) != LocationQuality.GOOD) {
        // Define a listener that responds to location updates
        LocationListener locationListener = new LocationListener() {

            public void onLocationChanged(Location location) {
                updateBestLocation(location);
                if (desiredAccuracy != 0
                        && getLocationQuality(desiredAccuracy,
                                acceptedAccuracy, maxAge, bestLocation)
                                == LocationQuality.GOOD)
                    stopLooper();
            }

            public void onProviderEnabled(String provider) {
                if (isSameProvider(provider,
                        LocationManager.NETWORK_PROVIDER))networkEnabled =true;
                else if (isSameProvider(provider,
                        LocationManager.GPS_PROVIDER)) gpsEnabled = true;
                // The user has enabled a location, remove any error
                // notification
                if (canUseGps && gpsEnabled || canUseNetwork
                        && networkEnabled) removeErrorNotification();
            }

            public void onProviderDisabled(String provider) {
                if (isSameProvider(provider,
                        LocationManager.NETWORK_PROVIDER))networkEnabled=false;
                else if (isSameProvider(provider,
                        LocationManager.GPS_PROVIDER)) gpsEnabled = false;
                if (!gpsEnabled && !networkEnabled) {
                    showErrorNotification();
                    stopLooper();
                }
            }

            public void onStatusChanged(String provider, int status,
                    Bundle extras) {
                Log.i(LOG_TAG, "Provider " + provider + " statusChanged");
                if (isSameProvider(provider,
                        LocationManager.NETWORK_PROVIDER)) networkEnabled = 
                        status == LocationProvider.AVAILABLE
                        || status == LocationProvider.TEMPORARILY_UNAVAILABLE;
                else if (isSameProvider(provider,
                        LocationManager.GPS_PROVIDER))
                    gpsEnabled = status == LocationProvider.AVAILABLE
                      || status == LocationProvider.TEMPORARILY_UNAVAILABLE;
                // None of them are available, stop listening
                if (!networkEnabled && !gpsEnabled) {
                    showErrorNotification();
                    stopLooper();
                }
                // The user has enabled a location, remove any error
                // notification
                else if (canUseGps && gpsEnabled || canUseNetwork
                        && networkEnabled) removeErrorNotification();
            }
        };
        if (networkEnabled || gpsEnabled) {
            Looper.prepare();
            setLooper(Looper.myLooper());
            // Register the listener with the Location Manager to receive
            // location updates
            if (canUseGps)
                locationManager.requestLocationUpdates(
                        LocationManager.GPS_PROVIDER, 1000, 1,
                        locationListener, Looper.myLooper());
            if (canUseNetwork)
                locationManager.requestLocationUpdates(
                        LocationManager.NETWORK_PROVIDER, 1000, 1,
                        locationListener, Looper.myLooper());
            Timer t = new Timer();
            t.schedule(new TimerTask() {

                @Override
                public void run() {
                    stopLooper();
                }
            }, maxPollingTime * 1000);
            Looper.loop();
            t.cancel();
            setLooper(null);
            locationManager.removeUpdates(locationListener);
        } else // No provider is enabled, show a notification
        showErrorNotification();
    }
    if (getLocationQuality(desiredAccuracy, acceptedAccuracy, maxAge,
            bestLocation) != LocationQuality.BAD) {
        sendUpdate(new Event(EVENT_TYPE, locationToString(desiredAccuracy,
                acceptedAccuracy, maxAge, bestLocation)));
    } else Log.w(LOG_TAG, "LocationCollector failed to get a location");
}

private synchronized void showErrorNotification() {
    if (notifId != 0) return;
    ServiceHandler handler = service.getHandler();
    NotificationInfo ni = NotificationInfo.createSingleNotification(
            R.string.locationcollector_notif_ticker,
            R.string.locationcollector_notif_title,
            R.string.locationcollector_notif_text,
            android.R.drawable.stat_notify_error);
    Intent intent = new Intent(
            android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
    ni.pendingIntent = PendingIntent.getActivity(service, 0, intent,
            PendingIntent.FLAG_UPDATE_CURRENT);
    Message msg = handler.obtainMessage(ServiceHandler.SHOW_NOTIFICATION);
    msg.obj = ni;
    handler.sendMessage(msg);
    notifId = ni.id;
}

private void removeErrorNotification() {
    if (notifId == 0) return;
    ServiceHandler handler = service.getHandler();
    if (handler != null) {
        Message msg = handler.obtainMessage(
                ServiceHandler.CLEAR_NOTIFICATION, notifId, 0);
        handler.sendMessage(msg);
        notifId = 0;
    }
}

@Override
public void interrupt() {
    stopLooper();
    super.interrupt();
}

private String locationToString(int desiredAccuracy, int acceptedAccuracy,
        int maxAge, Location location) {
    StringBuilder sb = new StringBuilder();
    sb.append(String.format(
            "qual=%s time=%d prov=%s acc=%.1f lat=%f long=%f",
            getLocationQuality(desiredAccuracy, acceptedAccuracy, maxAge,
                    location), location.getTime() / 1000, // Millis to
                                                            // seconds
            location.getProvider(), location.getAccuracy(), location
                    .getLatitude(), location.getLongitude()));
    if (location.hasAltitude())
        sb.append(String.format(" alt=%.1f", location.getAltitude()));
    if (location.hasBearing())
        sb.append(String.format(" bearing=%.2f", location.getBearing()));
    return sb.toString();
}

private enum LocationQuality {
    BAD, ACCEPTED, GOOD;

    public String toString() {
        if (this == GOOD) return "Good";
        else if (this == ACCEPTED) return "Accepted";
        else return "Bad";
    }
}

private LocationQuality getLocationQuality(int desiredAccuracy,
        int acceptedAccuracy, int maxAge, Location location) {
    if (location == null) return LocationQuality.BAD;
    if (!location.hasAccuracy()) return LocationQuality.BAD;
    long currentTime = System.currentTimeMillis();
    if (currentTime - location.getTime() < maxAge * 1000
            && location.getAccuracy() <= desiredAccuracy)
        return LocationQuality.GOOD;
    if (acceptedAccuracy == -1
            || location.getAccuracy() <= acceptedAccuracy)
        return LocationQuality.ACCEPTED;
    return LocationQuality.BAD;
}

private synchronized void updateBestLocation(Location location) {
    bestLocation = getBestLocation(location, bestLocation);
}

protected Location getBestLocation(Location location,
        Location currentBestLocation) {
    if (currentBestLocation == null) {
        // A new location is always better than no location
        return location;
    }
    if (location == null) return currentBestLocation;
    // Check whether the new location fix is newer or older
    long timeDelta = location.getTime() - currentBestLocation.getTime();
    boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
    boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
    boolean isNewer = timeDelta > 0;
    // If it's been more than two minutes since the current location, use
    // the new location
    // because the user has likely moved
    if (isSignificantlyNewer) {
        return location;
        // If the new location is more than two minutes older, it must be
        // worse
    } else if (isSignificantlyOlder) {
        return currentBestLocation;
    }
    // Check whether the new location fix is more or less accurate
    int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation
            .getAccuracy());
    boolean isLessAccurate = accuracyDelta > 0;
    boolean isMoreAccurate = accuracyDelta < 0;
    boolean isSignificantlyLessAccurate = accuracyDelta > 200;
    // Check if the old and new location are from the same provider
    boolean isFromSameProvider = isSameProvider(location.getProvider(),
            currentBestLocation.getProvider());
    // Determine location quality using a combination of timeliness and
    // accuracy
    if (isMoreAccurate) {
        return location;
    } else if (isNewer && !isLessAccurate) {
        return location;
    } else if (isNewer && !isSignificantlyLessAccurate
            && isFromSameProvider) {
        return location;
    }
    return bestLocation;
}

/** Checks whether two providers are the same */
private boolean isSameProvider(String provider1, String provider2) {
    if (provider1 == null) return provider2 == null;
    return provider1.equals(provider2);
}

Cześć Nicklas, mam taką samą równowagę, więc mógłbym się z tobą skontaktować w jakikolwiek sposób .. Byłbym ci wdzięczny, gdybyś mógł nam pomóc ..
School Boy

Czy możesz napisać cały kod? Dzięki, bardzo mile widziane
rodi

To jest cały kod. Nie mam już dostępu do projektu.
Nicklas A.,

1
Wygląda na to, że wziąłeś kod tego projektu „android-protips-location” i wciąż żyje. Ludzie mogą zobaczyć, jak to działa tutaj code.google.com/p/android-protips-location/source/browse/trunk/…
Gödel77

7

Dokładność lokalizacji zależy głównie od zastosowanego dostawcy lokalizacji:

  1. GPS - zapewni ci kilka metrów dokładności (zakładając, że masz odbiór GPS)
  2. Wi-Fi - zapewni ci kilkaset metrów dokładności
  3. Cell Network - Otrzymasz bardzo niedokładne wyniki (widziałem odchylenie do 4 km ...)

Jeśli szukasz dokładności, jedyną opcją jest GPS.

Czytałem artykuł bardzo dobrze poinformowany o tym tutaj .

Jeśli chodzi o limit czasu GPS - 60 sekund powinno wystarczyć, aw większości przypadków nawet za dużo. Myślę, że 30 sekund jest OK, a czasem nawet mniej niż 5 sekund ...

jeśli potrzebujesz tylko jednej lokalizacji, sugeruję, że w twojej onLocationChangedmetodzie, po otrzymaniu aktualizacji wyrejestrujesz słuchacza i unikniesz niepotrzebnego korzystania z GPS.


Tak naprawdę nie dbam o to, skąd mam swoją lokalizację, nie chcę ograniczać się do jednego dostawcy
Nicklas A.

Możesz zarejestrować wszystkich dostawców lokalizacji dostępnych na urządzeniu (możesz uzyskać listę wszystkich dostawców z LocationManager.getProviders ()), ale jeśli szukasz dokładnej poprawki, w większości przypadków dostawca sieci nie będzie dla ciebie przydatny.
Muzikant,

Tak, ale to nie jest pytanie o wybór pomiędzy dostawcami, jest to pytanie ogólnie o uzyskanie najlepszej lokalizacji (nawet gdy w grę wchodzi wielu dostawców)
Nicklas A.,

4

Obecnie używam, ponieważ jest to godne zaufania, aby uzyskać lokalizację i obliczać odległość dla mojej aplikacji ...... używam tego do mojej aplikacji taksówkowej.

użyj interfejsu API fusion opracowanego przez programistę Google z fuzją czujnika GPS, magnetometru, akcelerometru, a także za pomocą Wi-Fi lub lokalizacji komórki, aby obliczyć lub oszacować lokalizację. Jest również w stanie dokładnie aktualizować lokalizację również w budynku. w celu uzyskania szczegółowych informacji przejdź do linku https://developers.google.com/android/reference/com/google/android/gms/location/FusedLocationProviderApi

import android.app.Activity;
import android.location.Location;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;

import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


public class MainActivity extends Activity implements LocationListener,
        GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener {

    private static final long ONE_MIN = 500;
    private static final long TWO_MIN = 500;
    private static final long FIVE_MIN = 500;
    private static final long POLLING_FREQ = 1000 * 20;
    private static final long FASTEST_UPDATE_FREQ = 1000 * 5;
    private static final float MIN_ACCURACY = 1.0f;
    private static final float MIN_LAST_READ_ACCURACY = 1;

    private LocationRequest mLocationRequest;
    private Location mBestReading;
TextView tv;
    private GoogleApiClient mGoogleApiClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (!servicesAvailable()) {
            finish();
        }

        setContentView(R.layout.activity_main);
tv= (TextView) findViewById(R.id.tv1);
        mLocationRequest = LocationRequest.create();
        mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
        mLocationRequest.setInterval(POLLING_FREQ);
        mLocationRequest.setFastestInterval(FASTEST_UPDATE_FREQ);

        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addApi(LocationServices.API)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .build();


        if (mGoogleApiClient != null) {
            mGoogleApiClient.connect();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (mGoogleApiClient != null) {
            mGoogleApiClient.connect();
        }
    }

    @Override
    protected void onPause() {d
        super.onPause();

        if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
            mGoogleApiClient.disconnect();
        }
    }


        tv.setText(location + "");
        // Determine whether new location is better than current best
        // estimate
        if (null == mBestReading || location.getAccuracy() < mBestReading.getAccuracy()) {
            mBestReading = location;


            if (mBestReading.getAccuracy() < MIN_ACCURACY) {
                LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
            }
        }
    }

    @Override
    public void onConnected(Bundle dataBundle) {
        // Get first reading. Get additional location updates if necessary
        if (servicesAvailable()) {

            // Get best last location measurement meeting criteria
            mBestReading = bestLastKnownLocation(MIN_LAST_READ_ACCURACY, FIVE_MIN);

            if (null == mBestReading
                    || mBestReading.getAccuracy() > MIN_LAST_READ_ACCURACY
                    || mBestReading.getTime() < System.currentTimeMillis() - TWO_MIN) {

                LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, this);

               //Schedule a runnable to unregister location listeners

                    @Override
                    public void run() {
                        LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, MainActivity.this);

                    }

                }, ONE_MIN, TimeUnit.MILLISECONDS);

            }

        }
    }

    @Override
    public void onConnectionSuspended(int i) {

    }


    private Location bestLastKnownLocation(float minAccuracy, long minTime) {
        Location bestResult = null;
        float bestAccuracy = Float.MAX_VALUE;
        long bestTime = Long.MIN_VALUE;

        // Get the best most recent location currently available
        Location mCurrentLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
        //tv.setText(mCurrentLocation+"");
        if (mCurrentLocation != null) {
            float accuracy = mCurrentLocation.getAccuracy();
            long time = mCurrentLocation.getTime();

            if (accuracy < bestAccuracy) {
                bestResult = mCurrentLocation;
                bestAccuracy = accuracy;
                bestTime = time;
            }
        }

        // Return best reading or null
        if (bestAccuracy > minAccuracy || bestTime < minTime) {
            return null;
        }
        else {
            return bestResult;
        }
    }

    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {

    }

    private boolean servicesAvailable() {
        int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);

        if (ConnectionResult.SUCCESS == resultCode) {
            return true;
        }
        else {
            GooglePlayServicesUtil.getErrorDialog(resultCode, this, 0).show();
            return false;
        }
    }
}

2

Przeszukałem Internet w poszukiwaniu zaktualizowanej (w ubiegłym roku) odpowiedzi, używając najnowszych metod pobierania lokalizacji sugerowanych przez Google (aby użyć FusedLocationProviderClient). W końcu wylądowałem na tym:

https://github.com/googlesamples/android-play-location/tree/master/LocationUpdates

Stworzyłem nowy projekt i skopiowałem większość tego kodu. Bum. To działa. I myślę, że bez żadnych przestarzałych linii.

Ponadto symulator nie wydaje lokalizacji GPS, o której wiem. Dotarło aż do zgłoszenia tego w dzienniku: „Wszystkie ustawienia lokalizacji są spełnione”.

I na koniec, jeśli chcesz wiedzieć (ja to zrobiłem), NIE potrzebujesz klucza API map Google z konsoli programisty Google, jeśli wszystko, czego potrzebujesz, to lokalizacja GPS.

Przydatny jest także ich samouczek. Chciałem jednak pełnego samouczka / przykładowego kodu i tego. Ich samouczek kumuluje się, ale jest mylący, gdy jesteś w tym nowy, ponieważ nie wiesz, jakie elementy potrzebujesz z wcześniejszych stron.

https://developer.android.com/training/location/index.html

I na koniec pamiętaj takie rzeczy:

Nie tylko musiałem zmodyfikować mainActivity.Java. Musiałem także zmodyfikować Strings.xml, androidmanifest.xml ORAZ prawidłowy build.gradle. A także twoja aktywność_Main.xml (ale ta część była dla mnie łatwa).

Musiałem dodać takie zależności: implementacja „com.google.android.gms: play-services-location: 11.8.0” i zaktualizować ustawienia mojego zestawu SDK studia Android, aby uwzględnić usługi Google Play. (wygląd ustawień plików ustawienia systemowe Android SDK Narzędzia SDK sprawdź usługi Google Play).

aktualizacja: symulator Androida zdawał się uzyskiwać lokalizację i zdarzenia zmiany lokalizacji (kiedy zmieniłem wartość w ustawieniach karty SIM). Ale moje najlepsze i pierwsze wyniki były na rzeczywistym urządzeniu. Prawdopodobnie najłatwiej jest przetestować na rzeczywistych urządzeniach.


1

Niedawno przebudowano, aby uzyskać lokalizację kodu, poznać kilka dobrych pomysłów, a na koniec udało się uzyskać stosunkowo idealną bibliotekę i wersję demonstracyjną.

@ Odpowiedź Gryphiusa jest dobra

    //request all valid provider(network/gps)
private boolean requestAllProviderUpdates() {
    checkRuntimeEnvironment();
    checkPermission();

    if (isRequesting) {
        EasyLog.d("Request location update is busy");
        return false;
    }


    long minTime = getCheckTimeInterval();
    float minDistance = getCheckMinDistance();

    if (mMapLocationListeners == null) {
        mMapLocationListeners = new HashMap<>();
    }

    mValidProviders = getValidProviders();
    if (mValidProviders == null || mValidProviders.isEmpty()) {
        throw new IllegalArgumentException("Not available provider.");
    }

    for (String provider : mValidProviders) {
        LocationListener locationListener = new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                if (location == null) {
                    EasyLog.e("LocationListener callback location is null.");
                    return;
                }
                printf(location);
                mLastProviderTimestamp = location.getTime();

                if (location.getProvider().equals(LocationManager.GPS_PROVIDER)) {
                    finishResult(location);
                } else {
                    doLocationResult(location);
                }

                removeProvider(location.getProvider());
                if (isEmptyValidProviders()) {
                    requestTimeoutMsgInit();
                    removeUpdates();
                }
            }

            @Override
            public void onStatusChanged(String provider, int status, Bundle extras) {
            }

            @Override
            public void onProviderEnabled(String provider) {
            }

            @Override
            public void onProviderDisabled(String provider) {
            }
        };
        getLocationManager().requestLocationUpdates(provider, minTime, minDistance, locationListener);
        mMapLocationListeners.put(provider, locationListener);
        EasyLog.d("Location request %s provider update.", provider);
    }
    isRequesting = true;
    return true;
}

//remove request update
public void removeUpdates() {
    checkRuntimeEnvironment();

    LocationManager locationManager = getLocationManager();
    if (mMapLocationListeners != null) {
        Set<String> keys = mMapLocationListeners.keySet();
        for (String key : keys) {
            LocationListener locationListener = mMapLocationListeners.get(key);
            if (locationListener != null) {
                locationManager.removeUpdates(locationListener);
                EasyLog.d("Remove location update, provider is " + key);
            }
        }
        mMapLocationListeners.clear();
        isRequesting = false;
    }
}

//Compared with the last successful position, to determine whether you need to filter
private boolean isNeedFilter(Location location) {
    checkLocation(location);

    if (mLastLocation != null) {
        float distance = location.distanceTo(mLastLocation);
        if (distance < getCheckMinDistance()) {
            return true;
        }
        if (location.getAccuracy() >= mLastLocation.getAccuracy()
                && distance < location.getAccuracy()) {
            return true;
        }
        if (location.getTime() <= mLastProviderTimestamp) {
            return true;
        }
    }
    return false;
}

private void doLocationResult(Location location) {
    checkLocation(location);

    if (isNeedFilter(location)) {
        EasyLog.d("location need to filtered out, timestamp is " + location.getTime());
        finishResult(mLastLocation);
    } else {
        finishResult(location);
    }
}

//Return to the finished position
private void finishResult(Location location) {
    checkLocation(location);

    double latitude = location.getLatitude();
    double longitude = location.getLongitude();
    float accuracy = location.getAccuracy();
    long time = location.getTime();
    String provider = location.getProvider();

    if (mLocationResultListeners != null && !mLocationResultListeners.isEmpty()) {
        String format = "Location result:<%f, %f> Accuracy:%f Time:%d Provider:%s";
        EasyLog.i(String.format(format, latitude, longitude, accuracy, time, provider));

        mLastLocation = location;
        synchronized (this) {
            Iterator<LocationResultListener> iterator =  mLocationResultListeners.iterator();
            while (iterator.hasNext()) {
                LocationResultListener listener = iterator.next();
                if (listener != null) {
                    listener.onResult(location);
                }
                iterator.remove();
            }
        }
    }
}

Pełna implementacja: https://github.com/bingerz/FastLocation/blob/master/fastlocationlib/src/main/java/cn/bingerz/fastlocation/FastLocation.java

1. Dzięki pomysłom @Gryphius, udostępniam również cały kod.

2. Każde żądanie uzupełnienia lokalizacji najlepiej usunąć Aktualizacje, w przeciwnym razie na pasku stanu telefonu zawsze będzie wyświetlana ikona pozycjonowania


0

Z mojego doświadczenia wynika, że ​​najlepiej jest korzystać z poprawki GPS, chyba że jest ona niedostępna. Niewiele wiem o innych dostawcach lokalizacji, ale wiem, że w przypadku GPS istnieje kilka sztuczek, których można użyć, aby dać trochę dokładności w getcie. Wysokość jest często znakiem, więc możesz sprawdzić niedorzeczne wartości. Istnieje poprawka dokładności poprawek lokalizacji Androida. Również jeśli widzisz liczbę użytych satelitów, może to również wskazywać na precyzję.

Ciekawym sposobem na uzyskanie lepszego pomysłu na dokładność może być bardzo szybkie zapytanie o zestaw poprawek, na przykład ~ 1 / s przez 10 sekund, a następnie spanie przez minutę lub dwie. Jedna rozmowa, w której byłem, doprowadziła do przekonania, że ​​niektóre urządzenia z Androidem i tak to zrobią. Następnie usunąłbyś wartości odstające (słyszałem wspomniany tutaj filtr Kalmana) i zastosowałeś strategię centrowania, aby uzyskać pojedynczą poprawkę.

Oczywiście głębokość, którą tu osiągniesz, zależy od tego, jak ciężkie są twoje wymagania. Jeśli masz szczególnie surowe wymagania, aby uzyskać NAJLEPSZĄ możliwą lokalizację, myślę, że przekonasz się, że lokalizacja GPS i sieci są tak podobne jak jabłka i pomarańcze. Również GPS może się znacznie różnić od urządzenia do urządzenia.


Cóż, nie jest ważne, aby był najlepszy, wystarczy, że jest wystarczająco dobry, aby narysować mapę i że nie wyczerpuję baterii, ponieważ jest to zadanie w tle.
Nicklas A.,

-3

Skyhook (http://www.skyhookwireless.com/) ma dostawcę lokalizacji, który jest znacznie szybszy niż standardowy oferowany przez Google. To może być to, czego szukasz. Nie jestem z nimi związany.


Co ciekawe, wydaje się, że używają tylko WiFi, co jest bardzo miłe, ale nadal potrzebuję go do działania, gdy nie ma połączenia WiFi lub 3G / 2G, więc byłoby to dodanie kolejnej warstwy abstrakcji. Ale dobry chwyt.
Nicklas A.,

1
Skyhook wydaje się używać kombinacji Wi-Fi, GPS i wież komórkowych. Szczegóły techniczne można znaleźć na stronie skyhookwireless.com/howitworks . Odnieśli ostatnio kilka zwycięstw w projektowaniu, na przykład Mapquest, Twydroid, ShopSavvy i Sony NGP. Pamiętaj, że pobieranie i wypróbowanie ich zestawu SDK wydaje się być bezpłatne, ale musisz skontaktować się z nimi w sprawie licencji na rozpowszechnianie go w Twojej aplikacji. Niestety nie podają ceny na swojej stronie internetowej.
Ed Burnette,

Rozumiem. Cóż, jeśli komercyjne wykorzystanie nie jest darmowe, obawiam się, że nie mogę tego użyć.
Nicklas A.,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.