Ticket #2390: named.php

File named.php, 24.3 KB (added by woidrick, 10 years ago)

hack, sorry, cant use diff

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