Kullanıcının Android'de yerini almanın iyi bir yolu


211

Sorun:

Kullanıcının geçerli konumunu bir ASAP eşiğinde tutmak ve aynı zamanda pil tasarrufu yapmak.

Sorun neden bir sorundur:

Öncelikle, android'in iki sağlayıcısı var; şebeke ve GPS. Bazen ağ daha iyi, bazen de GPS daha iyi olur.

"Daha iyi" ile hız / doğruluk oranını kastediyorum.
Konumu neredeyse anında ve GPS'i açmadan alabiliyorum, birkaç metre doğruluktan feda etmeye hazırım.

İkinci olarak, konum değişiklikleri için güncelleme talep ederseniz, mevcut konum kararlıysa hiçbir şey gönderilmez.

Google'ın burada "en iyi" konumu belirlemeye bir örneği var: http://developer.android.com/guide/topics/location/obtaining-user-location.html#BestEstimate
Ama bence olması gerektiği kadar iyi bir yer yok /olabilirdi.

Google'ın konum için normalleştirilmiş bir API'ya sahip olmadığından, geliştiricinin konumun nereden geldiğini umursamayacağına, sadece ne istediğinizi belirtmeniz ve telefonun sizin için seçmesi gerektiğine inanıyorum.

Yardıma ihtiyacım olan şey:

Belki bazı sezgisel ya da belki bazı 3. parti kütüphane aracılığıyla "en iyi" yerini belirlemek için iyi bir yol bulmak gerekir.

Bu, en iyi sağlayıcıyı belirlemek anlamına gelmez!
Muhtemelen tüm sağlayıcıları kullanacağım ve en iyisini seçeceğim.

Uygulamanın arka planı:

Uygulama, kullanıcının konumunu sabit bir aralıkta toplar (her 10 dakikada bir söyleyelim) ve bir sunucuya gönderir.
Uygulama mümkün olduğunca fazla pil tasarrufu sağlamalı ve konum X (50-100?) Metre hassasiyete sahip olmalıdır.

Amaç daha sonra gün içinde kullanıcının yolunu bir harita üzerinde çizebilmek ve böylece bunun için yeterli doğruluğa ihtiyacım var.

Çeşitli:

Sizce istenen ve kabul edilen doğruluklar için makul değerler nelerdir?
Kabul edilen 100m ve istenildiği gibi 30m kullanıyorum, bu çok sormak için mi?
Kullanıcının yolunu daha sonra bir harita üzerinde çizebilmek istiyorum.
İstenilen 100m ve kabul edilenler için 500m daha mı iyi?

Ayrıca, şu anda konum güncellemesi başına maksimum 60 saniye için GPS açık, iç mekanda belki 200m doğrulukta bir yer almak için çok kısa mı?


Bu benim mevcut kodum, herhangi bir geri bildirim takdir (TODO olan hata kontrolü eksikliği dışında):

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
Gerçekten geç saatlerde yayınlanıyor, ancak kısa süre önce IO 2013'te açıklanan "Kaynaşmış Konum Sağlayıcı", ihtiyaçlarınızın çoğunu karşılıyor gibi görünüyor - developer.android.com/google/play-services/location.html
Matt

getBestLocation () öğesinin son satırı olmamalıdır: return currentBestLocation; dönüş yerine bestLocation ;?
Gavriel

Yanıtlar:


164

Görünüşe göre aynı uygulamayı kodluyoruz ;-)
İşte benim mevcut uygulama. Hala GPS yükleyici uygulamamın beta test aşamasındayım, bu yüzden birçok olası iyileştirme olabilir. ama şimdiye kadar gayet iyi çalışıyor gibi görünüyor.

/**
 * 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;
}

Düzenle: konum sağlayıcılarından periyodik güncellemeler isteyen kısım:

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
}

Düşünülmesi gereken şeyler:

  • GPS güncellemelerini çok sık istemeyin, pil gücünü tüketir. Şu anda benim uygulama için varsayılan olarak 30 dk kullanıyorum.

  • 'bilinen son konuma minimum mesafe' kontrolü ekleyin. bu olmadan, GPS mevcut olmadığında ve konum baz istasyonlarından trianguüle edildiğinde puanlarınız "atlayacaktır". veya yeni konumun bilinen son konumdan gelen doğruluk değerinin dışında olup olmadığını kontrol edebilirsiniz.


2
Asla yeni bir konum elde edemezsiniz, sadece önceki güncellemelerde olan yerleri kullanırsınız. Bu kod aslında zaman zaman GPS açarak konumu güncelleştiren bir dinleyici ekleyerek büyük yarar sağlayacağını düşünüyorum.
Nicklas A.

2
üzgünüm, sadece mevcut tüm yerlerden en iyisini seçen bölümle ilgilendiğinizi düşündüm. Ben de bu istekleri yukarıda kodu ekledi. yeni bir gps konumu alınırsa, hemen depolanır / yüklenir. bir ağ konumu güncellemesi alırsam referans için saklarım ve bir sonraki konum kontrolü gerçekleşene kadar bir gps güncellemesi alacağımı umuyorum.
Gryphius

2
Ayrıca zamanlayıcı iptal bir stopRecording () yöntemi vardı. Sonunda şimdi temelde executor.shutdown () ve de-kayıtlarını tüm konum güncelleme dinleyiciler çağırır stopRecording, bir ScheduledThreadPoolExecutor için bir zamanlayıcı gelen anahtarlamalı
Gryphius

1
scm'ye göre, stopRecording sadece gpsTimer.cancel () olarak adlandırılır ve gps_recorder_running = false olarak ayarlanır, böylece sizin durumunuzda olduğu gibi, dinleyicilerin temizlenmesi gerekmez. mevcut kod bir vektör içindeki tüm aktif dinleyicileri takip eder, 1.5 yıl önce bu cevabı yazarken buna sahip değildim.
Gryphius

1
zaten github üzerinde , ama bu hala GPS şeyler bugünlerde yapmak için en iyi yol olduğundan emin değilim. Afaik, bu kodu yazdığımdan beri konum API'sında birçok iyileştirme yaptılar.
Gryphius

33

Uygulamanız için doğru konum sağlayıcıyı seçmek için Ölçüt nesnelerini kullanabilirsiniz :

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); 

Bağımsız değişkenlerin nasıl dikkate alındığı hakkında daha fazla bilgi için requestLocationUpdates belgesini okuyun :

Bildirim sıklığı minTime ve minDistance parametreleri kullanılarak kontrol edilebilir. MinTime 0'dan büyükse, LocationManager güçten tasarruf etmek için konum güncellemeleri arasında minTime milisaniye boyunca bekleyebilir. MinDistance 0'dan büyükse, bir konum yalnızca cihaz minDistance metre ile hareket ederse yayınlanır. Bildirimleri olabildiğince sık almak için her iki parametreyi de 0 olarak ayarlayın.

Daha fazla düşünce

  • Konum nesnelerinin doğruluğunu metre cinsinden konumun tahmini doğruluğunu döndüren Location.getAccuracy () ile izleyebilirsiniz .
  • Criteria.ACCURACY_HIGHkriter GPS olabildiğince iyi olarak değil, ihtiyaçlarınızı eşleşen, 100 m aşağıda hataları vermelidir.
  • Ayrıca, konum sağlayıcınızın durumunu izlemeniz ve kullanıcı tarafından kullanılamaz veya devre dışı bırakılırsa başka bir sağlayıcıya geçmeniz gerekir.
  • Pasif sağlayıcı ayrıca başvurunun bu tür için iyi bir eşleşme olabilir: Fikir onlar başka uygulama ve yayın systemwideye tarafından istenen her yere güncellemelerini kullanmaktır.

Ben içine baktım Criteriaama ne en son ağ konumu (wifi üzerinden bilmek olabilir) harika ve onu almak için zaman veya pil (getLastKnown) alırsanız, o zaman kriterler muhtemelen dikkate almaz ve bunun yerine GPS döndürür. Google'ın bunu geliştiriciler için zorlaştırdığına inanamıyorum.
Nicklas A.

Ölçütleri kullanmanın yanı sıra, seçtiğiniz sağlayıcı tarafından gönderilen her konum güncellemesinde, GPS sağlayıcısı için lastKnowLocation'ı kontrol edebilir ve mevcut konumunuzla (doğruluk ve tarih) karşılaştırabilirsiniz. Ama bu bana şartnamelerinizin bir gereksinimi yerine sahip olması hoş bir şey gibi görünüyor; bazen biraz daha iyi doğruluk elde edilirse, kullanıcılarınız için gerçekten yararlı olur mu?
Stéphane

Şu anda yaptığım şey bu, son bilmenin yeterince iyi olup olmadığını anlamakta zorlanıyorum. Kendimi bir sağlayıcıyla sınırlamak zorunda olmadığımı da ekleyebilirim, ne kadar çok kullanırsam o kadar hızlı kilit alabilirim.
Nicklas A.11

PASSIVE_PROVIDER'ın API Seviye 8 veya daha yüksek bir sürüm gerektirdiğini unutmayın.
Eduardo

@ Stéphane düzenleme için üzgünüm. Onunla ilgilenme. Gönderiniz doğru. Bu düzenlemeyi hata için yaptım. Afedersiniz. Saygılarımızla.
Gaucho

10

İlk iki noktayı cevaplamak :

  • GPS , etkinleştirilmişse ve çevresinde kalın duvarlar yoksa size her zaman daha kesin bir konum verecektir .

  • Konum değişmediyse, getLastKnownLocation (String) öğesini çağırabilir ve konumu hemen alabilirsiniz.

Alternatif bir yaklaşım kullanmak :

Kullanımdaki hücre kimliğini veya tüm komşu hücreleri almayı deneyebilirsiniz

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()));
}

Ardından birkaç açık veri tabanı üzerinden hücre konumuna başvurabilirsiniz (örneğin, http://www.location-api.com/ veya http://opencellid.org/ )


Strateji, yeri okurken kule kimlikleri listesini okumak olacaktır. Ardından, bir sonraki sorguda (uygulamanızda 10 dakika) bunları tekrar okuyun. En azından bazı kuleler aynı ise, kullanımı güvenlidir getLastKnownLocation(String). Değilse, bekleyin onLocationChanged(). Bu konum için bir üçüncü taraf veritabanı ihtiyacını ortadan kaldırır. Bu yaklaşımı da deneyebilirsiniz .


Evet, ama ben lastKnownLocation gerçekten kötü ise sorun geliyor. İki konumun en iyisine karar vermenin iyi bir yoluna ihtiyacım var.
Nicklas A.

Kulelerin bilgisini saklayabilir ve bu kulelerin değişip değişmediğini kontrol edebilirsiniz. Eğer öyleyse, yeni bir yer bekleyin, eğer değilse (veya sadece bazıları değiştiyse), tekrar kullanın. Bu şekilde kule konumlarını bir veritabanıyla karşılaştırmaktan kaçınırsınız.
Aleadam

Kuleleri kullanmak benim için büyük bir abartı gibi görünüyor, yine de iyi bir fikir.
Nicklas A.

@Nicklas kodu bundan daha karmaşık hale gelmez. Yine de android.Manifest.permission # ACCESS_COARSE_UPDATES'e ihtiyacınız olacak.
Aleadam

Evet, ama yine de bir üçüncü taraf hizmeti kullanmam gerekiyor ve ayrıca konum bilgisi üzerinde kule bilgisini ne zaman kullanacağına karar vermenin bir yoluna ihtiyacım var, bu sadece ekstra bir karmaşıklık katmanı ekliyor.
Nicklas A.

9

Bu oldukça iyi çalışan benim çözümüm:

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);
}

Merhaba Nicklas ben aynı eşitlik var, bu yüzden size herhangi bir yolla iletişim kurabilir miyim .. bize yardımcı olabilir eğer size tam teşekkür ederim ..
School Boy

Kodun tamamını gönderebilir misiniz? Teşekkürler, gerçekten takdir
ediliyor

Tüm kod bu. Artık projeye erişimim yok.
Nicklas A.

1
Bu proje "android-protips-location" kodunu almış gibi görünüyor ve hala yaşıyor. İnsanlar burada nasıl çalıştığını görebilirler code.google.com/p/android-protips-location/source/browse/trunk/…
Gödel77

7

Konum doğruluğu çoğunlukla kullanılan konum sağlayıcısına bağlıdır:

  1. GPS - size birkaç metre doğruluk sağlar (GPS alımınız olduğu varsayılarak)
  2. Kablosuz - Size birkaç yüz metre doğruluk kazandıracak
  3. Hücre Ağı - Size yanlış sonuçlar verecek (4km'ye kadar sapma gördüm ...)

Aradığınız doğruluk ise, tek seçeneğiniz GPS'dir.

Burada çok bilgilendirici bir makale okudum .

GPS zaman aşımına gelince - 60 saniye yeterli ve çoğu durumda çok fazla olmalıdır. Sanırım 30 saniye iyi ve hatta bazen 5 saniyeden az ...

yalnızca tek bir konuma ihtiyacınız varsa, onLocationChangedyönteminizde, bir güncelleme aldıktan sonra dinleyicinin kaydını iptal edeceğiniz ve GPS'in gereksiz kullanımından kaçınacağınızı öneririm .


Konumumu nereden aldığım gerçekten umrumda değil, beni bir sağlayıcıyla sınırlamak istemiyorum
Nicklas A.

Cihazda bulunan tüm konum sağlayıcılarını kaydedebilirsiniz (tüm sağlayıcıların listesini LocationManager.getProviders () adresinden alabilirsiniz), ancak doğru bir düzeltme arıyorsanız, çoğu durumda ağ sağlayıcısı sizin için yararlı olmaz.
Muzikant

Evet, ancak bu sağlayıcılar arasında seçim yapmakla ilgili bir soru değil, bu genellikle en iyi konumu elde etmekle ilgili bir soru (birden fazla sağlayıcı olsa bile)
Nicklas A. 22

4

Şu anda bu konum almak ve benim uygulama için mesafe hesaplamak için güvenilir olduğundan kullanıyorum ...... ben benim taksi uygulaması için kullanıyorum.

Google geliştiricinin GPS Sensörü, Manyetometre, İvmeölçer füzyonu ile geliştirdiği füzyon API'sını kullanarak konumu hesaplamak veya tahmin etmek için Wifi veya hücre konumunu da kullanın. Ayrıca bina içinde de konum güncellemelerini doğru bir şekilde verebilir. ayrıntılar için https://developers.google.com/android/reference/com/google/android/gms/location/FusedLocationProviderApi bağlantısına ulaşın

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

Ben google tarafından önerilen (FusedLocationProviderClient kullanmak için) en son konum çekme yöntemlerini kullanarak güncellenmiş (geçen yıl) bir cevap için internet ovuşturdu. Sonunda buna indim:

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

Yeni bir proje oluşturdum ve bu kodun çoğuna kopyaladım. Boom. İşe yarıyor. Ve bence herhangi bir kullanım dışı çizgi olmadan.

Ayrıca, simülatör bildiğim bir GPS konumu almıyor gibi görünüyor. Günlüğe bunu bildiren kadar ilerledi: "Tüm konum ayarları karşılandı."

Ve son olarak, bilmek istediğim (yaptım), tek istediğiniz GPS konumu ise, google geliştirici konsolundan bir google maps api anahtarına ihtiyacınız YOKTUR.

Ayrıca öğretici yararlıdır. Ama tam bir sayfa öğretici / kod örneği istedim ve bu. Onların öğretici yığınları ama bu yeni olduğunuzda kafa karıştırıcı çünkü daha önceki sayfalardan hangi parçalara ihtiyacınız olduğunu bilmiyorsunuz.

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

Son olarak, bunun gibi şeyleri hatırlayın:

Ben sadece mainActivity.Java değiştirmek zorunda kaldım. Ayrıca Strings.xml, androidmanifest.xml ve doğru build.gradle değiştirmek zorunda kaldı. Ayrıca activity_Main.xml (ancak bu bölüm benim için kolaydı).

Bunun gibi bağımlılıklar eklemem gerekiyordu: application 'com.google.android.gms: play-services-location: 11.8.0' ve google play hizmetlerini dahil etmek için android studio SDK ayarlarımı güncellemeliydim. (dosya ayarları görünüm sistem ayarları android SDK SDK Araçları kontrol google oyun hizmetleri).

Güncelleme: Android simülatörü bir konum ve konum değişikliği olayları (sim ayarlarındaki değeri değiştirdiğimde) alıyordu. Ama en iyi ve ilk sonuçlarım gerçek bir cihazdaydı. Bu yüzden gerçek cihazları test etmek muhtemelen en kolay yöntemdir.


1

Son zamanlarda kodun yerini almak, bazı iyi fikirler öğrenmek ve nihayet nispeten mükemmel bir kütüphane ve Demo elde etmek için yeniden.

@ Gryphius'un cevabı iyi

    //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();
            }
        }
    }
}

Tam uygulama: https://github.com/bingerz/FastLocation/blob/master/fastlocationlib/src/main/java/cn/bingerz/fastlocation/FastLocation.java

1.Teşekkürler @Gryphius çözüm fikirleri, ben de tam kodu paylaşmak.

2.Her yeri tamamlamak için her istek, kaldırmak en iyisidirGüncellemeler, aksi takdirde telefon durum çubuğu her zaman konumlandırma simgesini görüntüler


0

Deneyimlerime göre, mevcut değilse GPS düzeltmesi ile gitmek için en iyi buldum. Diğer konum sağlayıcıları hakkında fazla bir şey bilmiyorum, ancak GPS için bir getto hassas ölçüsü vermek için kullanılabilecek birkaç püf noktası olduğunu biliyorum. Yükseklik genellikle bir işarettir, bu nedenle saçma değerleri kontrol edebilirsiniz. Android konum düzeltmelerinde doğruluk ölçümü var. Ayrıca, kullanılan uydu sayısını görebiliyorsanız, bu aynı zamanda hassasiyeti de gösterebilir.

Doğruluk hakkında daha iyi bir fikir edinmenin ilginç bir yolu, 10 saniye boyunca ~ 1 / sn gibi bir dizi düzeltmeyi çok hızlı bir şekilde istemek ve ardından bir veya iki dakika uyumak olabilir. Yaptığım bir konuşma, bazı android cihazların bunu zaten yapacağına inanmaya yol açtı. Daha sonra aykırı otları ayıklayacaksınız (burada bahsedilen Kalman filtresini duydum) ve tek bir düzeltme elde etmek için bir tür merkezleme stratejisi kullanırsınız.

Açıkçası burada elde ettiğiniz derinlik, gereksinimlerinizin ne kadar zor olduğuna bağlıdır. Mümkün olan en iyi yeri elde etmek için özellikle katı bir gereksiniminiz varsa, GPS ve ağ konumunun elma ve portakal kadar benzer olduğunu göreceksiniz. Ayrıca GPS cihazdan cihaza çılgınca farklı olabilir.


Bunun en iyisi olması önemli değil, sadece bir harita üzerinde çizmek için yeterince iyi olması ve pili boşaltmamam, çünkü bu bir arka plan görevi.
Nicklas A.

-3

Skyhook (http://www.skyhookwireless.com/), Google'ın sağladığı standarttan çok daha hızlı bir konum sağlayıcısına sahiptir. Aradığın şey bu olabilir. Onlara bağlı değilim.


İlginç, onlar sadece çok güzel ancak WiFi kullanmak gibi görünüyor ama yine de hiçbir wifi veya 3G / 2G bağlantı etrafında çalışırken bu yüzden bu soyutlama başka bir katman ekleyerek olurdu. Gerçi iyi yakalamak.
Nicklas A.

1
Skyhook'ta WiFi, GPS ve baz istasyonları kullandığı görülüyor. Teknik ayrıntılar için skyhookwireless.com/howitworks adresine bakın . Son zamanlarda birkaç tasarım galibiyeti aldılar, örneğin Mapquest, Twydroid, ShopSavvy ve Sony NGP. SDK'larını indirmenin ve denemenin ücretsiz gibi göründüğünü, ancak uygulamanızda dağıtmak için bir lisans hakkında onlarla iletişime geçmeniz gerektiğini unutmayın. Ne yazık ki kendi web sitesinde fiyat listelemez.
Ed Burnette

Ah anlıyorum. Ticari olarak kullanmak serbest değilse, korkarım ki kullanamam.
Nicklas A.
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.