Comment puis-je implémenter une recherche basée sur la localisation (code postal) dans WordPress?

19

Je travaille sur un site d'annuaire d'entreprise local qui utilisera des types de publication personnalisés pour les entrées d'entreprise. L'un des champs sera "Code postal". Comment puis-je configurer une recherche basée sur la position?

J'aimerais que les visiteurs puissent entrer leur code postal et choisir une catégorie et afficher toutes les entreprises ayant un certain rayon, ou toutes les entreprises commandées par distance. J'ai vu quelques plugins qui prétendent le faire, mais ils ne prennent pas en charge WordPress 3.0. Aucune suggestion?

mat
la source
2
Je commence une prime parce que c'est une question intéressante et stimulante. J'ai moi-même quelques idées ... mais je veux voir si quelqu'un peut trouver quelque chose de plus élégant (et plus facile à construire).
EAMann
Merci EAMann. Cela peut aider: briancray.com/2009/04/01/…
matt
la seule suggestion que j'aurais actuellement ici est d'utiliser un plugin tel que des pods
NetConstructor.com
@ NetConstructor.com - Je ne suggérerais pas de pods pour cela; il n'y a vraiment aucun avantage que Pods offre ce problème par rapport aux types de publication personnalisés.
MikeSchinkel
@matt : J'ai récemment implémenté quelque chose de très similaire à cela bien que le site ne soit pas encore finalisé ni déployé. Il y en a un peu aussi, en fait. Je prévois de le regrouper en tant que plugin de localisation de magasin à un moment donné, mais ce n'est pas encore quelque chose que je peux publier en tant que solution générale. Contactez-moi hors ligne et je pourrai peut-être vous aider si vous n'obtenez pas la réponse dont vous avez besoin.
MikeSchinkel

Réponses:

10

Je modifierais la réponse de gabrielk et du billet de blog lié en utilisant des index de base de données et en minimisant le nombre de calculs de distance réels .

Si vous connaissez les coordonnées de l'utilisateur et que vous connaissez la distance maximale (disons 10 km), vous pouvez dessiner un cadre de délimitation de 20 km sur 20 km avec l'emplacement actuel au milieu. Obtenez ces coordonnées de délimitation et interrogez uniquement les magasins entre ces latitudes et longitudes . N'utilisez pas encore de fonctions de trigonométrie dans votre requête de base de données, car cela empêcherait l'utilisation d'index. (Vous pouvez donc obtenir un magasin à 12 km de vous s'il se trouve dans le coin nord-est du cadre de délimitation, mais nous le jetons à l'étape suivante.)

Calculez uniquement la distance (à vol d'oiseau ou avec les itinéraires réels, selon votre préférence) pour les quelques magasins retournés. Cela améliorera considérablement le temps de traitement si vous avez un grand nombre de magasins.

Pour la recherche associée ( "donnez les dix magasins les plus proches" ), vous pouvez effectuer une recherche similaire, mais avec une estimation de la distance initiale (vous commencez donc avec une zone de 10 km sur 10 km, et si vous n'avez pas assez de magasins, vous la développez pour 20 km sur 20 km et ainsi de suite). Pour cette distance initiale, vous devez calculer le nombre de magasins sur la surface totale et l'utiliser. Ou enregistrez le nombre de requêtes nécessaires et adaptez-vous au fil du temps.

J'ai ajouté un exemple de code complet à la question connexe de Mike , et voici une extension qui vous donne les emplacements X les plus proches (rapides et à peine testés):

class Monkeyman_Geo_ClosestX extends Monkeyman_Geo
{
    public static $closestXStartDistanceKm = 10;
    public static $closestXMaxDistanceKm = 1000; // Don't search beyond this

    public function addAdminPages()
    {
        parent::addAdminPages();
        add_management_page( 'Location closest test', 'Location closest test', 'edit_posts', __FILE__ . 'closesttest', array(&$this, 'doClosestTestPage'));
    }

    public function doClosestTestPage()
    {
        if (!array_key_exists('search', $_REQUEST)) {
            $default_lat = ini_get('date.default_latitude');
            $default_lon = ini_get('date.default_longitude');

            echo <<<EOF
<form action="" method="post">
    <p>Number of posts: <input size="5" name="post_count" value="10"/></p>
    <p>Center latitude: <input size="10" name="center_lat" value="{$default_lat}"/>
        <br/>Center longitude: <input size="10" name="center_lon" value="{$default_lon}"/></p>
    <p><input type="submit" name="search" value="Search!"/></p>
</form>
EOF;
            return;
        }
        $post_count = intval($_REQUEST['post_count']);
        $center_lon = floatval($_REQUEST['center_lon']);
        $center_lat = floatval($_REQUEST['center_lat']);

        var_dump(self::getClosestXPosts($center_lon, $center_lat, $post_count));
    }

    /**
     * Get the closest X posts to a given location
     *
     * This might return more than X results, and never more than
     * self::$closestXMaxDistanceKm away (to prevent endless searching)
     * The results are sorted by distance
     *
     * The algorithm starts with all locations no further than
     * self::$closestXStartDistanceKm, and then grows this area
     * (by doubling the distance) until enough matches are found.
     *
     * The number of expensive calculations should be minimized.
     */
    public static function getClosestXPosts($center_lon, $center_lat, $post_count)
    {
        $search_distance = self::$closestXStartDistanceKm;
        $close_posts = array();
        while (count($close_posts) < $post_count && $search_distance < self::$closestXMaxDistanceKm) {
            list($north_lat, $east_lon, $south_lat, $west_lon) = self::getBoundingBox($center_lat, $center_lon, $search_distance);

            $geo_posts = self::getPostsInBoundingBox($north_lat, $east_lon, $south_lat, $west_lon);


            foreach ($geo_posts as $geo_post) {
                if (array_key_exists($geo_post->post_id, $close_posts)) {
                    continue;
                }
                $post_lat = floatval($geo_post->lat);
                $post_lon = floatval($geo_post->lon);
                $post_distance = self::calculateDistanceKm($center_lat, $center_lon, $post_lat, $post_lon);
                if ($post_distance < $search_distance) {
                    // Only include those that are in the the circle radius, not bounding box, otherwise we might miss some closer in the next step
                    $close_posts[$geo_post->post_id] = $post_distance;
                }
            }

            $search_distance *= 2;
        }

        asort($close_posts);

        return $close_posts;
    }

}

$monkeyman_Geo_ClosestX_instace = new Monkeyman_Geo_ClosestX();
Jan Fabry
la source
8

Vous avez d'abord besoin d'une table qui ressemble à ceci:

zip_code    lat     lon
10001       40.77    73.98

... renseigné pour chaque code postal. Vous pouvez développer cela en ajoutant des champs de ville et d'état si vous souhaitez rechercher de cette façon.

Ensuite, chaque magasin peut recevoir un code postal, et lorsque vous devez calculer la distance, vous pouvez joindre le tableau lat / long aux données du magasin.

Ensuite, vous interrogerez ce tableau pour obtenir la latitude et la longitude du magasin et des codes postaux de l'utilisateur. Une fois que vous l'avez obtenu, vous pouvez remplir votre tableau et le passer à une fonction "obtenir la distance":

$user_location = array(
    'latitude' => 42.75,
    'longitude' => 73.80,
);

$output = array();
$results = $wpdb->get_results("SELECT id, zip_code, lat, lon FROM store_table");
foreach ( $results as $store ) {
    $store_location = array(
        'zip_code' => $store->zip_code, // 10001
        'latitude' => $store->lat, // 40.77
        'longitude' => $store->lon, // 73.98
    );

    $distance = get_distance($store_location, $user_location, 'miles');

    $output[$distance][$store->id] = $store_location;
}

ksort($output);

foreach ($output as $distance => $store) {
    foreach ( $store as $id => $location ) {
        echo 'Store ' . $id . ' is ' . $distance . ' away';
    }
}

function get_distance($store_location, $user_location, $units = 'miles') {
    if ( $store_location['longitude'] == $user_location['longitude'] &&
    $store_location['latitude'] == $user_location['latitude']){
        return 0;

    $theta = ($store_location['longitude'] - $user_location['longitude']);
    $distance = sin(deg2rad($store_location['latitude'])) * sin(deg2rad($user_location['latitude'])) + cos(deg2rad($store_location['latitude'])) * cos(deg2rad($user_location['latitude'])) * cos(deg2rad($theta));
    $distance = acos($distance);
    $distance = rad2deg($distance);
    $distance = $distance * 60 * 1.1515;

    if ( 'kilometers' == $units ) {
        $distance = $distance * 1.609344;
    }

    return round($distance);
}

Il s'agit d'une preuve de concept, et non d'un code que je recommanderais réellement de mettre en œuvre. Si vous avez 10 000 magasins, par exemple, ce serait une opération assez coûteuse de les interroger tous et de les parcourir et de les trier à chaque demande.

Gabrielk
la source
Les résultats peuvent-ils être mis en cache? En outre, serait-il plus facile d'interroger l'une des bases de données de code postal disponibles dans le commerce (ou gratuites s'il y en a une)?
mat
1
@matt - Qu'entendez-vous par interroger l'un des produits disponibles dans le commerce ou gratuits? La mise en cache devrait toujours être possible, voir codex.wordpress.org/Transients_API
hakre
@hakre: Nevermind, je pense que je parle au-dessus de ma tête maintenant. Je parle d'utiliser une base de données de code postal (USPS, Google Maps ...) pour obtenir les distances mais je ne savais pas qu'ils ne stockent probablement pas les distances, ils stockent juste le code postal et les coordonnées et il le fera à moi de le calculer.
mat
3

La documentation MySQL contient également des informations sur les extensions spatiales. Curieusement, la fonction standard distance () n'est pas disponible, mais consultez cette page: http://dev.mysql.com/tech-resources/articles/4.1/gis-with-mysql.html pour plus de détails sur la façon de "convertir" les deux valeurs POINT à un LINESTRING, puis calculer la longueur de cela. "

Notez que chaque fournisseur est susceptible d'offrir différentes latitudes et longitudes représentant le "centroïde" d'un code postal. Il vaut également la peine de savoir qu'il n'y a pas de véritable fichier de «limite» de code postal défini. Chaque fournisseur aura son propre ensemble de limites qui correspondent à peu près aux listes spécifiques d'adresses qui composent un code postal USPS. (Par exemple, dans certaines "limites", vous devrez inclure les deux côtés de la rue, dans d'autres, un seul.) Les zones de tabulation de code postal (ZCTA), largement utilisées par les fournisseurs, "ne représentent pas précisément les zones de livraison de code postal, et n'incluent pas tous les codes postaux utilisés pour la distribution du courrier " http://www.census.gov/geo/www/cob/zt_metadata.html

De nombreuses entreprises du centre-ville auront leur propre code postal. Vous voudrez un ensemble de données aussi complet que possible, alors assurez-vous de trouver une liste de codes postaux qui comprend à la fois des codes postaux "ponctuels" (généralement des entreprises) et des codes postaux "limites".

J'ai de l'expérience avec les données du code postal de http://www.semaphorecorp.com/ . Même cela n'était pas précis à 100%. Par exemple, lorsque mon campus a pris une nouvelle adresse postale et un nouveau code postal, le code postal a été égaré. Cela dit, c'était la seule source de données que j'ai trouvée qui avait même le nouveau code postal, donc peu de temps après sa création.

J'avais une recette dans mon livre pour savoir exactement comment répondre à votre demande ... à Drupal. Il s'appuyait sur le module Google Maps Tools ( http://drupal.org/project/gmaps , à ne pas confondre avec http://drupal.org/project/gmap , également un module digne.) Vous pourriez trouver quelques exemples utiles code dans ces modules, bien sûr, ils ne fonctionneront pas hors de la boîte dans WordPress.

Marjorie Roswell
la source