source: subversion/sites/namefinder/php/named.php @ 4135

Last change on this file since 4135 was 4134, checked in by david, 12 years ago

Main application files

File size: 23.5 KB
Line 
1<?php 
2
3class named {
4
5  /* The main class of the application. A 'named' is an item on the
6     map (way,segment or node) which has a name or something like a name
7     (a road number reference or airport IATA code for example). Named maps to a
8     database table.
9
10     The class is supplemented by placeindex, which duplicates some of
11     the information here so is strictly speaking unnecessary. However
12     when qualifying a search by place name is is moreefficient to
13     search a small index of place names than a general index of
14     everything.  */
15
16  /* --- The following fields are stored in the database --- */
17  var $id;       /* a number unique across all nameds, unlike the osm
18                    id which is only unique within type. This id is
19                    the osm id multiplied by 10 and then the type
20                    inserted in the low order decimal digit (1 for
21                    node, 2 for segement, 3 for way. It's done this
22                    way to make it easy to see the correspondence when
23                    seeing the number, and therefore to
24                    debug. Conversion functions in canon.php */
25  var $region;   /* A number identifying the region of the earth where
26                    the point associated with named is. Allows us to
27                    limit database searches geographically, and it is
28                    this more than anything which makes the
29                    application efficient. See region.php for =details
30                    of how this number is computed from lat/lon */
31  var $lat;      /* in the case of a way, position of one of the nodes
32                    along it, determined by the indexing utility */
33  var $lon;   
34  var $name;     /* the name: but includes alternatives like the road
35                    number in square brackets, e.g. "Main Street
36                    [A1134]", and language equivalents, e.g. "London
37                    [fr:Londres]" */
38  var $canon;    /* A canonical equivalent of the name to make it easy
39                    to search for with variation. It also includes the
40                    type (e.g. 'school') so we can search for
41                    'school'as well as a specific school. By
42                    converting names to canonical form during
43                    indexing, and then converting the users search
44                    term to canonical form too, we can hope to find a
45                    match. See canon.php for more details on the
46                    canonicalisation. */
47  var $category; /* The kind of item represented by the named - the
48                    tag name of the main tag of the object, such as
49                    'highway'or 'amenity'. This is used to ensure that
50                    we keep nearby similarly named objects of
51                    different kinds -otherwise we cull nearby duplicates */
52  var $is_in;    /* A tidied up version of the is_in string coming from the
53                    OSM data where supplied (usually for places only) */
54  var $rank;     /* A number saying whether a named is a place and if
55                    so how important it is: 0 for not a place, 10 for
56                    hamlet, 20 for village and so on. The rank is used
57                    particularly in determining the most sensible
58                    contextual information to supply */
59  var $info;     /* desription of the kind of named: basically a
60                    version of the value part of the main tag
61                    describing named, for example 'school', tidied up
62                    for readability - we quote this, as in 'school
63                    <name> found...'. Note it is _not_ used in
64                    searches like 'schools near <somewhere>' - that
65                    comes from comparing with canon above */
66 
67  /* --- the following fields are not stored in the database --- */
68  var $place;    /* Any named we found has a reference to the named
69                    which is the place we used as the context of the
70                    search,if any. For example, in "Hinton Road,
71                    Fulbourn", when named represents Hinton Road,
72                    place would be the named for Fulbourn */
73  var $distance; /* The distance in km to named referencing this one, if any. In the
74                    above example, Fulbourn would say how far it is from its
75                    referencing named Hinton Road */
76  var $direction; /* Direction in degrees anticlockwise from east (the
77                    x axis) _to_ the referencing named */
78  var $placenearer; /* A named which is the nearest place to the the
79                    named other than place (if one and the same, this
80                    is not set). For example, "Hinton Road, Cambridge"
81                    will find Fulbourn is the placennearer, but place
82                    is what was requested in the search and is
83                    Cambridge */
84  var $nearesttown1; /* a named which is the nearest town to this named, which
85                    will be a place, if this named is not a town itself */
86  var $nearesttown2; /* likewise, nearest city if not a city itself */
87  var $description; /* The textual description of the result incuding all its context */
88  var $zoom;     /* A suggested zoom level at which the resultmight be
89                    displayed, accodng to the kind of named it is:
90                    cities for example wil be well zoomed out, but
91                    hotels are zoomed all the way in */
92
93  // --------------------------------------------------
94  function localdistancefrom($named) {
95    /* this is the cartesian distance in a flat projection, so
96       approximate and only valid locally. 111.0 is the equivalent of
97       one degree of latitude (and one of longitude atthe equator) */
98    $dlat = ($this->lat - $named->lat)*111.0;
99    $dlon = ($this->lon - $named->lon)*sin(deg2rad(90.0-abs($named->lat)))*111.0;
100    $this->distance = sqrt(pow($dlon,2) + pow($dlat,2));
101    if (abs($dlon) < 0.0001) {
102      $this->direction = $dlat > 0.0 ? 90 : 270;
103    } else {
104      $this->direction = (int)(round(rad2deg(atan($dlat/$dlon))));
105      // gives +90 -> -90
106      if ($dlon < 0) { $this->direction -= 180; }
107      while ($this->direction < 0) { $this->direction += 360; }
108    }
109  }
110
111  // --------------------------------------------------
112  function approxkm() {
113    /* it is more readable to give approximate distances between
114       places, especially as the preciselocation of the centre of a town
115       etc is unimportant, and a street has lots of possible locations
116       and the one we chose to represent it is arbitrary */
117    $distance = $this->distance;
118    if ($distance < 0.9) { return 0; }
119    if ($distance < 35.0) { 
120      $distance = (int)floor($distance + 0.5);
121    } else if ($distance < 75.0) {
122      /* nearest 5km */
123      $distance = 5 * (int)floor(($distance + 2.5)/5.0);
124    } else {
125      /* nearest 10km */
126      $distance = 10 * (int)floor(($distance + 5.0)/10.0);
127    }
128    return $distance;
129  }
130
131  // --------------------------------------------------
132  function approxdistance() {
133    /* converts approxkm into words */
134    $distance = $this->approxkm();
135    return $distance == 0 ? 'less than 1km' : "about {$distance}km";
136  }
137
138  // --------------------------------------------------
139  function findplace($otherthanplace=NULL, $rank=0) {
140    /* uses the place index to find the nearest place to this (excluding this,
141       of course, if it is a place). Usually used via findnearestplace below.
142
143       otherthanplace: a named which we wish to exclude from the search
144
145       rank: if non-zero only locates places for the particular rank
146       given. Can also be an array of ranks in which case we restrict
147       the search to any of those ranks
148
149       returns the named for the found place, or null if none found */
150
151    global $db;
152    include_once('placeindex.php');
153    $placeindex = placeindex::placeindexfromnamed($this);
154
155    /* limit the search to the two nearest places; we only want one,
156       but the first may be 'otherthanplace' which we want to exclude,
157       and it is more efficient to get two to start with than do
158       another search if the a single result happens to be
159       otherthanplace */
160    $nearbyplaces = $placeindex->findnearbyplaces($rank, 2);
161
162    foreach ($nearbyplaces as $placeindex) {
163      if (! empty($otherthanplace) && $placeindex->id == $otherthanplace->id) { continue; }
164      /* we found a place (probably nearer to the name we located than the one we were
165         searching for if any) */
166      $named = new named();
167      $named->id = $placeindex->id;
168      if ($db->select($named) != 1) {
169        $db->log("didn't find expected named from placeindex " . print_r($placeindex, 1));
170        return NULL;
171      }
172      // $db->log("findplace: ".print_r($named,1));
173      return $named;
174    }
175    return NULL;
176  }
177
178  // --------------------------------------------------
179  function findseveralplaces($rank, $maxresults) {
180    /* Like findplace above, but locates a series of nearby places (excluding
181       this, if it is a place) in order of distance from this.
182
183       rank: if non-zero, limits the search to places of that rank, or
184       if an array, any of those ranks given in the array
185
186       maxresults: the maximum nmber of results returned
187
188       returns: an array of nearby nameds for the places matched, which may be
189       empty if none found
190    */
191
192    global $db;
193    include_once('placeindex.php');
194    $placeindex = placeindex::placeindexfromnamed($this);
195    $nearbyplaces = $placeindex->findnearbyplaces($rank, $maxresults);
196    foreach ($nearbyplaces as $placeindex) {
197      $named = new named();
198      $named->id = $placeindex->id;
199      if ($db->select($named) != 1) {
200        $db->log("didn't find expected named from placeindex " . print_r($placeindex, 1));
201        return NULL;
202      }
203      $namedplaces[] = $named;
204    }
205    return $namedplaces;
206  }
207
208  // --------------------------------------------------
209  function findnearestplace($otherthanplace=NULL, $rank=0) {
210    /* findplace (q.v. for parameters) does the serious work; this function
211       just updates this with the result */
212    $place =& $this->findplace($otherthanplace, $rank);
213    if (! empty($place)) {
214      $this->placenearer =& $place;
215      $this->placenearer->localdistancefrom($this);
216      $this->placenearer->assigncontext();
217    }
218  }
219
220  // --------------------------------------------------
221  function assigncontext() {
222    /* Sets the nearesttown context for this */
223    if ($this->rank == 0) { return; /* not a real place */}
224
225    /* have we seen it before? Rather than look upeverything in the
226       database,we keep a cache because the nearest place to A is also
227       often the nearest place to to nearby place B */
228    static $placecache = array();
229    if (! empty($placecache[$this->id])) {
230      if (! empty($placecache[$this->id]->nearesttown1)) { 
231        $this->nearesttown1 =& $placecache[$this->id]->nearesttown1;
232      }
233      if (! empty($placecache[$this->id]->nearesttown2)) { 
234        $this->nearesttown2 =& $placecache[$this->id]->nearesttown2;
235      }
236      return;
237    }
238
239    /* we want a city if this is a town or city, but for lesser places a
240       town and/or city */
241    $ranktown = named::placerank('town');
242    $rankcity = named::placerank('city');
243
244    if ($this->rank <= $ranktown) {
245      $place =& $this->findplace(NULL, array($ranktown, $rankcity));
246      if (! empty($place)) { 
247        $place->localdistancefrom($this); 
248        if ($place->rank == $ranktown) {
249          $this->nearesttown1 =& $place;
250          $this->nearesttown2 =& $this->findplace(NULL, $rankcity);
251          if (! empty($this->nearesttown2)) { $this->nearesttown2->localdistancefrom($this); }
252        } else {
253          $this->nearesttown1 =& $place;
254        }
255      }
256    } else if ($this->rank == $rankcity) {
257      $this->nearesttown1 =& $this->findplace(NULL, $rankcity);
258      if (! empty($this->nearesttown1)) { 
259        $this->nearesttown1->localdistancefrom($this); 
260      } else {
261        /* din't find a city; is there a nearby town? */
262        $this->nearesttown1 =& $this->findplace(NULL, $ranktown);
263        if (! empty($this->nearesttown1)) { 
264          $this->nearesttown1->localdistancefrom($this); 
265        }
266      }
267    }
268
269    $placecache[$this->id] =& $this;
270  }
271
272  //--------------------------------------------------
273  /* static */ function getplacerankings() {
274    /* space allowed for expansion in between existing features */
275    static $placerankings = array(
276      'hamlet'=> 10, 'village'=>20, 'suburb'=>30, 
277      'town'=>50, 'small town' => 50, 
278      'city'=>60, 'metropolis' => 70);
279    return $placerankings;
280  }
281
282  // --------------------------------------------------
283  /* static */ function placerank($type) {
284    static $placerankings = NULL;
285    if (is_null($placerankings)) { $placerankings = named::getplacerankings(); }
286    return empty($placerankings[$type]) ? 0 : $placerankings[$type];
287  }
288
289  // --------------------------------------------------
290  function isolatedplaceneighbourranks() {
291    static $placerankings = NULL;
292    if (is_null($placerankings)) { $placerankings = named::getplacerankings(); }
293    switch ($this->rank) {
294    case $placerankings['village']:
295    case $placerankings['hamlet']:
296    default:
297      return array($placerankings['village'], $placerankings['town'], $placerankings['city']);
298    case $placerankings['suburb']:
299      return array($placerankings['suburb'], $placerankings['town'], $placerankings['city']);
300    case $placerankings['town']:
301    case $placerankings['city']:
302      return array($placerankings['town'], $placerankings['city']);
303    }
304  }
305
306  // --------------------------------------------------
307  function tidyupisin($resetisin=FALSE) {
308    /* is_in in osm is a bit untidy, with random spaces or not; just make it a bit tidier
309       returns: the improved string for this's is_in
310     */
311    static $previousisin = '';
312    if ($resetisin) { $previousisin = ''; }
313    if (empty($this->is_in)) { return ''; }
314    $isin = str_replace(';', ',', $this->is_in);
315    $explosion = explode(',', $isin);
316    $isin = '';
317    $prefix = '';
318    for($i = 0; $i < count($explosion); $i++) {
319      $term = trim($explosion[$i]);
320      if ($term == '') { continue; }
321      $isin .= $prefix . strtoupper($term{0}) . substr($term,1);
322      $prefix = ', ';
323    }
324    $isin = preg_replace('/\\,? *capital cities */i', '', $isin);
325    if ($isin == $previousisin) { return ', ditto'; }
326    $previousisin = $isin;
327    return " in {$isin}";
328  }
329
330  // --------------------------------------------------
331  /* static */ function lookupplaces($name, $latlon=NULL, $exact=FALSE) {
332    /* Returns as an array all places (that is, nameds with non-zero
333       rank) in the database which canonically match the non-canonical
334       name given
335
336       Can be constrained by a lat/lon bounding box - an array of 4 numbers south-west
337       lat,lon and north-east lat,lon, if latlon is non-null
338    */
339    global $db;
340    include_once('canon.php');
341    $places = array();
342    $q = $db->query();
343    $ands = array();
344    if (! empty($latlon) && is_array($latlon) && count($latlon) == 4) {
345      $ands[] = y_op::gt('lat',$latlon[0]);
346      $ands[] = y_op::gt('lon',$latlon[1]);
347      $ands[] = y_op::lt('lat',$latlon[2]);
348      $ands[] = y_op::lt('lon',$latlon[3]);
349    }
350    $canon = canon::canonical_with_synonym($name);
351    if (empty($canon)) { return $places; /* empty array */ }
352    $ands[] = canon::likecanon($canon, $exact);
353    $ands[] = y_op::gt('rank',0);
354    $q->where(y_op::aand($ands));
355
356    $placecandidate = new named();
357    while ($q->select($placecandidate) > 0) {
358      $places[] = clone $placecandidate;
359    }
360    return $places;
361  }
362
363  /* ==================================================
364     The following set of functions is used to derive a readable
365     description of the location of this in relation to nearby places */
366
367  // --------------------------------------------------
368  function describebasic($resetisin=FALSE) {
369    /* Builds the basic elements of this's description, returning the
370       result as a string resetisin: when TRUE avoids putting 'ditto'
371       when is_in is the same as the most recent one, so the first
372       time in each result of a search we would need that */
373    $info = $this->info;
374    if ($info == 'airport; airport') { $info = 'airport'; /* until I can fix it in the index */}
375    $infohtml = htmlspecialchars($info, ENT_QUOTES, 'UTF-8');
376    $namehtml = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
377    $isinhtml = htmlspecialchars($this->info, ENT_QUOTES, 'UTF-8');
378    $isinhtml = htmlspecialchars($this->tidyupisin($resetisin), ENT_QUOTES, 'UTF-8');
379    return "{$infohtml} &lt;strong&gt;{$namehtml}&lt;/strong&gt;{$isinhtml}";
380  }
381
382  // --------------------------------------------------
383  function describedistancefrom() {
384    /* Converts exact distance and direction in degrees of this to its
385       referencing named to an approximate distance and compass point
386       (_from_ the referening named), for example "less than 1km east
387       of the middle of" or "20km south-west of".  Returns a string */
388    $angle = $this->direction;
389    if ($angle <= 20 || $angle >= 340) { $direction = 'west'; }
390    else if ($angle <= 70) { $direction = 'south-west'; }
391    else if ($angle <= 110) { $direction = 'south'; }
392    else if ($angle <= 160) { $direction = 'south-east'; }
393    else if ($angle <= 200) { $direction = 'east'; }
394    else if ($angle <= 250) { $direction = 'north-east'; }
395    else if ($angle <= 290) { $direction = 'north'; }
396    else { $direction = 'north-west'; }
397
398    $approxdistance = $this->approxdistance();
399    return $approxdistance . ' ' . $direction . 
400      ($this->distance < 3.0 && $this->name != '' ? ' of middle of' : ' of');
401  }
402
403  // --------------------------------------------------
404  function describeincontext($resetisin=FALSE) {
405    /* Builds the complete contextual string for this from the above building blocks */
406    $s = $this->describebasic($resetisin);
407    $prefix = ' (which is ';
408    $andprefix = ' and ';
409    if (isset($this->nearesttown1)) {
410      $s .= $prefix . $this->nearesttown1->describedistancefrom() . ' ' . 
411        $this->nearesttown1->describebasic();
412      $prefix = $andprefix;
413    }
414    if (isset($this->nearesttown2)) {
415      $s .= $prefix . $this->nearesttown2->describedistancefrom() . ' ' . 
416        $this->nearesttown2->describebasic();
417      $prefix = $andprefix;
418    }
419    if ($prefix == $andprefix) { $s .= ')'; }
420    return $s;
421  }
422
423  // --------------------------------------------------
424  function contextcontains($place) {
425    /* returns a boolean indicating whether the named's place (that
426     is, the one the user was using as context to limit the search) is
427     the same as either of the nearesttown fields - so we don't need
428     to mention it twice in the description */
429    if (empty($place)) { return FALSE; }
430    if (! empty($this->nearesttown1) && $this->nearesttown1->id == $place->id) { return TRUE; }
431    if (! empty($this->nearesttown2) && $this->nearesttown2->id == $place->id) { return TRUE; }
432    return FALSE;
433  }
434
435  // --------------------------------------------------
436  function assigndescription($placerequested) {
437    /* Uses the building blocks above to set the description field and also zoom of this.
438       Returns nothing */
439    $prefix = '';
440    $s = $this->describeincontext(TRUE) . ' found ';
441    if (isset($this->placenearer) && 
442        ! $this->contextcontains($this->placenearer) && 
443       $this->placenearer->id != $this->id) 
444    {
445      $s .= $this->placenearer->describedistancefrom() . ' ' . 
446            $this->placenearer->describeincontext();
447      $prefix = ' and ';
448    }
449    if (is_object($this->place) && 
450        ! $this->contextcontains($this->place))
451    {
452      $s .= $prefix . 
453            $this->place->describedistancefrom() . ' ' . 
454            $this->place->describeincontext();
455    }
456
457    $this->description = $s;
458
459    $zoomlevels = array(
460                        'default'=>16,
461                        'water'=>13,
462                        'school'=>17,
463                        'university'=>17,
464                        'college'=>17,
465                        'cinema'=>17,
466                        'theatre'=>17,
467                        'hotel'=>17,
468                        'parking'=>17,
469                        'supermarket'=>17,
470                        'hospital'=>17,
471                        'doctors'=>17,
472                        'pharmacy'=>17,
473                        'requested location'=>12,
474                        'country'=>5,
475                        'pub'=>17,
476                        'airport'=>14,
477                        'airport; airport'=>14,
478                        'city'=>10,
479                        'town'=>11,
480                        'village'=>13,
481                        'hamlet'=>14,
482                        'suburb'=>13);
483    $this->zoom = $zoomlevels[empty($zoomlevels[$this->info]) ? 'default' : $this->info];
484  }
485
486  // --------------------------------------------------
487  function getosmid(&$type) { 
488    /* Helper function to get the type and id of this */
489    include_once('canon.php');
490    return canon::getosmid($this->id, $type);
491  }
492
493  // --------------------------------------------------
494  function xmlise() {
495    /* Converts this to an equivalent xml element, which is
496       returned. Recurses to deal with subordinate nameds making up
497       the context of a search result */
498    $xml = '<named';
499    $id = $this->getosmid($type);
500    $xml .= sprintf(" type='%s' id='%d' lat='%f' lon='%f' name='%s' category='%s' rank='%d' region='%d' canon='%s'",
501                  $type, 
502                  $id, 
503                  $this->lat, 
504                  $this->lon, 
505                  htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8'),
506                  htmlspecialchars($this->category, ENT_QUOTES, 'UTF-8'),
507                  $this->rank, 
508                  $this->region,
509                  htmlspecialchars($this->canon, ENT_QUOTES, 'UTF-8')
510    );
511    if (! empty($this->is_in)) { 
512      $xml .= " is_in='".htmlspecialchars($this->tidyupisin(TRUE), ENT_QUOTES, 'UTF-8')."'"; }
513    if (! empty($this->info)) { 
514      $xml .= " info='".htmlspecialchars($this->info, ENT_QUOTES, 'UTF-8')."'"; }
515    if (! empty($this->distance)) { 
516      $xml .= sprintf(" distance='%f' approxdistance='%d'", $this->distance, $this->approxkm());
517    }
518    if (! empty($this->direction)) { 
519      $xml .= sprintf(" direction='%s'", htmlspecialchars($this->direction, ENT_QUOTES, 'UTF-8'));
520    }
521    if (! empty($this->zoom)) {
522      $xml .= " zoom='{$this->zoom}'";
523    }
524    $xml .= ">\n";
525    if (! empty($this->description)) {
526      // already htmlspecialchar
527      $xml .= "<description>{$this->description}</description>\n";
528    }
529    if (! empty($this->place)) {
530      $xml .= "<place>\n".$this->place->xmlise()."</place>\n";
531    }
532    if (! empty($this->nearesttown1) || ! empty($this->nearesttown2)) {
533      $xml .= "<nearestplaces>\n";
534      if (! empty($this->nearesttown1)) { $xml .= $this->nearesttown1->xmlise(); }
535      if (! empty($this->nearesttown2)) { $xml .= $this->nearesttown2->xmlise(); }
536      $xml .= "</nearestplaces>\n";
537    } else if (!empty($this->placenearer)) {
538      $xml .= "<nearestplaces>\n" . $this->placenearer->xmlise() . "</nearestplaces>\n";
539    }
540    $xml .= "</named>\n";
541    return $xml;
542  }
543
544}
545
546?>
Note: See TracBrowser for help on using the repository browser.