forked from hans/Nominatim
Reverse geocode include geometry
This commit is contained in:
119
lib/Geocode.php
119
lib/Geocode.php
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
require_once(CONST_BasePath.'/lib/PlaceLookup.php');
|
||||
|
||||
class Geocode
|
||||
{
|
||||
protected $oDB;
|
||||
@@ -1634,87 +1636,20 @@
|
||||
foreach($aSearchResults as $iResNum => $aResult)
|
||||
{
|
||||
// Default
|
||||
$fDiameter = 0.0001;
|
||||
$fDiameter = getResultDiameter($aResult);
|
||||
|
||||
if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
|
||||
&& $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
|
||||
$oPlaceLookup = new PlaceLookup($this->oDB);
|
||||
$oPlaceLookup->setIncludePolygonAsPoints($this->bIncludePolygonAsPoints);
|
||||
$oPlaceLookup->setIncludePolygonAsText($this->bIncludePolygonAsText);
|
||||
$oPlaceLookup->setIncludePolygonAsGeoJSON($this->bIncludePolygonAsGeoJSON);
|
||||
$oPlaceLookup->setIncludePolygonAsKML($this->bIncludePolygonAsKML);
|
||||
$oPlaceLookup->setIncludePolygonAsSVG($this->bIncludePolygonAsSVG);
|
||||
$oPlaceLookup->setPolygonSimplificationThreshold($this->fPolygonSimplificationThreshold);
|
||||
|
||||
$aOutlineResult = $oPlaceLookup->getOutlines($aResult['place_id'], $aResult['lon'], $aResult['lat'], $fDiameter/2);
|
||||
foreach($aOutlineResult as $k => $v)
|
||||
{
|
||||
$fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'];
|
||||
}
|
||||
elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
|
||||
&& $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
|
||||
{
|
||||
$fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
|
||||
}
|
||||
$fRadius = $fDiameter / 2;
|
||||
|
||||
if (CONST_Search_AreaPolygons)
|
||||
{
|
||||
// Get the bounding box and outline polygon
|
||||
$sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
|
||||
$sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
|
||||
$sSQL .= "ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,";
|
||||
$sSQL .= "ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon";
|
||||
if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
|
||||
if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
|
||||
if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
|
||||
if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
|
||||
$sFrom = " from placex where place_id = ".$aResult['place_id'];
|
||||
if ($this->fPolygonSimplificationThreshold > 0)
|
||||
{
|
||||
$sSQL .= " from (select place_id,centroid,ST_SimplifyPreserveTopology(geometry,".$this->fPolygonSimplificationThreshold.") as geometry".$sFrom.") as plx";
|
||||
}
|
||||
else
|
||||
{
|
||||
$sSQL .= $sFrom;
|
||||
}
|
||||
|
||||
$aPointPolygon = $this->oDB->getRow($sSQL);
|
||||
if (PEAR::IsError($aPointPolygon))
|
||||
{
|
||||
failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
|
||||
}
|
||||
|
||||
if ($aPointPolygon['place_id'])
|
||||
{
|
||||
if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
|
||||
if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
|
||||
if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
|
||||
if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
|
||||
|
||||
if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
|
||||
{
|
||||
$aResult['lat'] = $aPointPolygon['centrelat'];
|
||||
$aResult['lon'] = $aPointPolygon['centrelon'];
|
||||
}
|
||||
|
||||
if ($this->bIncludePolygonAsPoints)
|
||||
{
|
||||
$aPolyPoints[] = geometryText2Points($aPointPolygon['astext'],$fRadius);
|
||||
|
||||
// Output data suitable for display (points and a bounding box)
|
||||
if (isset($aPolyPoints))
|
||||
{
|
||||
$aResult['aPolyPoints'] = array();
|
||||
foreach($aPolyPoints as $aPoint)
|
||||
{
|
||||
$aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (abs($aPointPolygon['minlat'] - $aPointPolygon['maxlat']) < 0.0000001)
|
||||
{
|
||||
$aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
|
||||
$aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
|
||||
}
|
||||
if (abs($aPointPolygon['minlon'] - $aPointPolygon['maxlon']) < 0.0000001)
|
||||
{
|
||||
$aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
|
||||
$aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
|
||||
}
|
||||
$aResult['aBoundingBox'] = array((string)$aPointPolygon['minlat'],(string)$aPointPolygon['maxlat'],(string)$aPointPolygon['minlon'],(string)$aPointPolygon['maxlon']);
|
||||
}
|
||||
$aResult[$k] = $v;
|
||||
}
|
||||
|
||||
if ($aResult['extra_place'] == 'city')
|
||||
@@ -1724,32 +1659,6 @@
|
||||
$aResult['rank_search'] = 16;
|
||||
}
|
||||
|
||||
if (!isset($aResult['aBoundingBox']))
|
||||
{
|
||||
$iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
|
||||
$fStepSize = (2*pi())/$iSteps;
|
||||
$aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
|
||||
$aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
|
||||
$aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
|
||||
$aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
|
||||
|
||||
// Output data suitable for display (points and a bounding box)
|
||||
if ($this->bIncludePolygonAsPoints)
|
||||
{
|
||||
$aPolyPoints = array();
|
||||
for($f = 0; $f < 2*pi(); $f += $fStepSize)
|
||||
{
|
||||
$aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
|
||||
}
|
||||
$aResult['aPolyPoints'] = array();
|
||||
foreach($aPolyPoints as $aPoint)
|
||||
{
|
||||
$aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
|
||||
}
|
||||
}
|
||||
$aResult['aBoundingBox'] = array((string)$aPointPolygon['minlat'],(string)$aPointPolygon['maxlat'],(string)$aPointPolygon['minlon'],(string)$aPointPolygon['maxlon']);
|
||||
}
|
||||
|
||||
// Is there an icon set for this type of result?
|
||||
if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
|
||||
&& $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
|
||||
protected $bNameDetails = false;
|
||||
|
||||
protected $bIncludePolygonAsPoints = false;
|
||||
protected $bIncludePolygonAsText = false;
|
||||
protected $bIncludePolygonAsGeoJSON = false;
|
||||
protected $bIncludePolygonAsKML = false;
|
||||
protected $bIncludePolygonAsSVG = false;
|
||||
protected $fPolygonSimplificationThreshold = 0.0;
|
||||
|
||||
|
||||
function PlaceLookup(&$oDB)
|
||||
{
|
||||
$this->oDB =& $oDB;
|
||||
@@ -46,6 +54,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setIncludePolygonAsPoints($b = true)
|
||||
{
|
||||
$this->bIncludePolygonAsPoints = $b;
|
||||
}
|
||||
|
||||
function getIncludePolygonAsPoints()
|
||||
{
|
||||
return $this->bIncludePolygonAsPoints;
|
||||
}
|
||||
|
||||
function setIncludePolygonAsText($b = true)
|
||||
{
|
||||
$this->bIncludePolygonAsText = $b;
|
||||
}
|
||||
|
||||
function getIncludePolygonAsText()
|
||||
{
|
||||
return $this->bIncludePolygonAsText;
|
||||
}
|
||||
|
||||
function setIncludePolygonAsGeoJSON($b = true)
|
||||
{
|
||||
$this->bIncludePolygonAsGeoJSON = $b;
|
||||
}
|
||||
|
||||
function setIncludePolygonAsKML($b = true)
|
||||
{
|
||||
$this->bIncludePolygonAsKML = $b;
|
||||
}
|
||||
|
||||
function setIncludePolygonAsSVG($b = true)
|
||||
{
|
||||
$this->bIncludePolygonAsSVG = $b;
|
||||
}
|
||||
|
||||
function setPolygonSimplificationThreshold($f)
|
||||
{
|
||||
$this->fPolygonSimplificationThreshold = $f;
|
||||
}
|
||||
|
||||
|
||||
function setPlaceID($iPlaceID)
|
||||
{
|
||||
$this->iPlaceID = $iPlaceID;
|
||||
@@ -219,5 +269,110 @@
|
||||
return $aAddress;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// returns an array which will contain the keys
|
||||
// aBoundingBox
|
||||
// and may also contain one or more of the keys
|
||||
// asgeojson
|
||||
// askml
|
||||
// assvg
|
||||
// astext
|
||||
// lat
|
||||
// lon
|
||||
function getOutlines($iPlaceID, $fLon=null, $fLat=null, $fRadius=null)
|
||||
{
|
||||
|
||||
$aOutlineResult = array();
|
||||
if (!$iPlaceID) return $aOutlineResult;
|
||||
|
||||
if (CONST_Search_AreaPolygons)
|
||||
{
|
||||
// Get the bounding box and outline polygon
|
||||
$sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
|
||||
$sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
|
||||
$sSQL .= "ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,";
|
||||
$sSQL .= "ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon";
|
||||
if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
|
||||
if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
|
||||
if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
|
||||
if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
|
||||
$sFrom = " from placex where place_id = ".$iPlaceID;
|
||||
if ($this->fPolygonSimplificationThreshold > 0)
|
||||
{
|
||||
$sSQL .= " from (select place_id,centroid,ST_SimplifyPreserveTopology(geometry,".$this->fPolygonSimplificationThreshold.") as geometry".$sFrom.") as plx";
|
||||
}
|
||||
else
|
||||
{
|
||||
$sSQL .= $sFrom;
|
||||
}
|
||||
|
||||
$aPointPolygon = $this->oDB->getRow($sSQL);
|
||||
if (PEAR::IsError($aPointPolygon))
|
||||
{
|
||||
echo var_dump($aPointPolygon);
|
||||
failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
|
||||
}
|
||||
|
||||
if ($aPointPolygon['place_id'])
|
||||
{
|
||||
if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
|
||||
{
|
||||
$aOutlineResult['lat'] = $aPointPolygon['centrelat'];
|
||||
$aOutlineResult['lon'] = $aPointPolygon['centrelon'];
|
||||
}
|
||||
|
||||
if ($this->bIncludePolygonAsGeoJSON) $aOutlineResult['asgeojson'] = $aPointPolygon['asgeojson'];
|
||||
if ($this->bIncludePolygonAsKML) $aOutlineResult['askml'] = $aPointPolygon['askml'];
|
||||
if ($this->bIncludePolygonAsSVG) $aOutlineResult['assvg'] = $aPointPolygon['assvg'];
|
||||
if ($this->bIncludePolygonAsText) $aOutlineResult['astext'] = $aPointPolygon['astext'];
|
||||
if ($this->bIncludePolygonAsPoints) $aOutlineResult['aPolyPoints'] = geometryText2Points($aPointPolygon['astext'], $fRadius);
|
||||
|
||||
|
||||
if (abs($aPointPolygon['minlat'] - $aPointPolygon['maxlat']) < 0.0000001)
|
||||
{
|
||||
$aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
|
||||
$aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
|
||||
}
|
||||
if (abs($aPointPolygon['minlon'] - $aPointPolygon['maxlon']) < 0.0000001)
|
||||
{
|
||||
$aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
|
||||
$aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
|
||||
}
|
||||
|
||||
$aOutlineResult['aBoundingBox'] = array(
|
||||
(string)$aPointPolygon['minlat'],
|
||||
(string)$aPointPolygon['maxlat'],
|
||||
(string)$aPointPolygon['minlon'],
|
||||
(string)$aPointPolygon['maxlon']
|
||||
);
|
||||
}
|
||||
} // CONST_Search_AreaPolygons
|
||||
|
||||
// as a fallback we generate a bounding box without knowing the size of the geometry
|
||||
if ( (!isset($aOutlineResult['aBoundingBox'])) && isset($fLon) )
|
||||
{
|
||||
|
||||
if ($this->bIncludePolygonAsPoints)
|
||||
{
|
||||
$sGeometryText = 'POINT('.$fLon.','.$fLat.')';
|
||||
$aOutlineResult['aPolyPoints'] = geometryText2Points($sGeometryText, $fRadius);
|
||||
}
|
||||
|
||||
$aBounds = array();
|
||||
$aBounds['minlat'] = $fLat - $fRadius;
|
||||
$aBounds['maxlat'] = $fLat + $fRadius;
|
||||
$aBounds['minlon'] = $fLon - $fRadius;
|
||||
$aBounds['maxlon'] = $fLon + $fRadius;
|
||||
|
||||
$aOutlineResult['aBoundingBox'] = array(
|
||||
(string)$aBounds['minlat'],
|
||||
(string)$aBounds['maxlat'],
|
||||
(string)$aBounds['minlon'],
|
||||
(string)$aBounds['maxlon']
|
||||
);
|
||||
}
|
||||
return $aOutlineResult;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
|
||||
protected $aLangPrefOrder = array();
|
||||
|
||||
protected $bIncludePolygonAsPoints = false;
|
||||
protected $bIncludePolygonAsText = false;
|
||||
protected $bIncludePolygonAsGeoJSON = false;
|
||||
protected $bIncludePolygonAsKML = false;
|
||||
protected $bIncludePolygonAsSVG = false;
|
||||
protected $fPolygonSimplificationThreshold = 0.0;
|
||||
|
||||
|
||||
function ReverseGeocode(&$oDB)
|
||||
{
|
||||
$this->oDB =& $oDB;
|
||||
@@ -58,6 +66,48 @@
|
||||
$this->iMaxRank = (isset($iZoom) && isset($aZoomRank[$iZoom]))?$aZoomRank[$iZoom]:28;
|
||||
}
|
||||
|
||||
function setIncludePolygonAsPoints($b = true)
|
||||
{
|
||||
$this->bIncludePolygonAsPoints = $b;
|
||||
}
|
||||
|
||||
function getIncludePolygonAsPoints()
|
||||
{
|
||||
return $this->bIncludePolygonAsPoints;
|
||||
}
|
||||
|
||||
function setIncludePolygonAsText($b = true)
|
||||
{
|
||||
$this->bIncludePolygonAsText = $b;
|
||||
}
|
||||
|
||||
function getIncludePolygonAsText()
|
||||
{
|
||||
return $this->bIncludePolygonAsText;
|
||||
}
|
||||
|
||||
function setIncludePolygonAsGeoJSON($b = true)
|
||||
{
|
||||
$this->bIncludePolygonAsGeoJSON = $b;
|
||||
}
|
||||
|
||||
function setIncludePolygonAsKML($b = true)
|
||||
{
|
||||
$this->bIncludePolygonAsKML = $b;
|
||||
}
|
||||
|
||||
function setIncludePolygonAsSVG($b = true)
|
||||
{
|
||||
$this->bIncludePolygonAsSVG = $b;
|
||||
}
|
||||
|
||||
function setPolygonSimplificationThreshold($f)
|
||||
{
|
||||
$this->fPolygonSimplificationThreshold = $f;
|
||||
}
|
||||
|
||||
// returns { place_id =>, type => '(osm|tiger)' }
|
||||
// fails if no place was found
|
||||
function lookup()
|
||||
{
|
||||
$sPointSQL = 'ST_SetSRID(ST_Point('.$this->fLon.','.$this->fLat.'),4326)';
|
||||
@@ -86,7 +136,8 @@
|
||||
if ($fSearchDiam > 0.008 && $iMaxRank > 22) $iMaxRank = 22;
|
||||
if ($fSearchDiam > 0.001 && $iMaxRank > 26) $iMaxRank = 26;
|
||||
|
||||
$sSQL = 'select place_id,parent_place_id,rank_search,calculated_country_code from placex';
|
||||
$sSQL = 'select place_id,parent_place_id,rank_search,calculated_country_code';
|
||||
$sSQL .= ' FROM placex';
|
||||
$sSQL .= ' WHERE ST_DWithin('.$sPointSQL.', geometry, '.$fSearchDiam.')';
|
||||
$sSQL .= ' and rank_search != 28 and rank_search >= '.$iMaxRank;
|
||||
$sSQL .= ' and (name is not null or housenumber is not null)';
|
||||
@@ -152,7 +203,11 @@
|
||||
{
|
||||
$iPlaceID = $iParentPlaceID;
|
||||
}
|
||||
$sSQL = "select address_place_id from place_addressline where place_id = $iPlaceID order by abs(cached_rank_address - $iMaxRank) asc,cached_rank_address desc,isaddress desc,distance desc limit 1";
|
||||
$sSQL = 'select address_place_id';
|
||||
$sSQL .= ' FROM place_addressline';
|
||||
$sSQL .= " WHERE place_id = $iPlaceID";
|
||||
$sSQL .= " ORDER BY abs(cached_rank_address - $iMaxRank) asc,cached_rank_address desc,isaddress desc,distance desc";
|
||||
$sSQL .= ' LIMIT 1';
|
||||
$iPlaceID = $this->oDB->getOne($sSQL);
|
||||
if (PEAR::IsError($iPlaceID))
|
||||
{
|
||||
@@ -165,7 +220,8 @@
|
||||
}
|
||||
|
||||
return array('place_id' => $iPlaceID,
|
||||
'type' => $bPlaceIsTiger ? 'tiger' : 'osm');
|
||||
'type' => $bPlaceIsTiger ? 'tiger' : 'osm');
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
|
||||
65
lib/lib.php
65
lib/lib.php
@@ -673,6 +673,31 @@
|
||||
return $aOrders;
|
||||
}
|
||||
|
||||
function getResultDiameter($aResult)
|
||||
{
|
||||
$aClassType = getClassTypes();
|
||||
|
||||
$fDiameter = 0.0001;
|
||||
|
||||
if (isset($aResult['class'])
|
||||
&& isset($aResult['type'])
|
||||
&& isset($aResult['admin_level'])
|
||||
&& isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
|
||||
&& $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
|
||||
{
|
||||
$fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'];
|
||||
}
|
||||
elseif (isset($aResult['class'])
|
||||
&& isset($aResult['type'])
|
||||
&& isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
|
||||
&& $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
|
||||
{
|
||||
$fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
|
||||
}
|
||||
|
||||
return $fDiameter;
|
||||
}
|
||||
|
||||
|
||||
function javascript_renderData($xVal, $iOptions = 0)
|
||||
{
|
||||
@@ -1024,27 +1049,47 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
function geometryText2Points($geometry_as_text,$fRadius)
|
||||
function geometryText2Points($geometry_as_text, $fRadius)
|
||||
{
|
||||
$aPolyPoints = NULL;
|
||||
if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$geometry_as_text,$aMatch))
|
||||
if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#', $geometry_as_text, $aMatch))
|
||||
{
|
||||
preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
|
||||
preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/', $aMatch[1], $aPolyPoints, PREG_SET_ORDER);
|
||||
}
|
||||
elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$geometry_as_text,$aMatch))
|
||||
elseif (preg_match('#LINESTRING\\(([- 0-9.,]+)#', $geometry_as_text, $aMatch))
|
||||
{
|
||||
preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
|
||||
preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/', $aMatch[1], $aPolyPoints, PREG_SET_ORDER);
|
||||
}
|
||||
elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$geometry_as_text,$aMatch))
|
||||
elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#', $geometry_as_text, $aMatch))
|
||||
{
|
||||
preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/', $aMatch[1], $aPolyPoints, PREG_SET_ORDER);
|
||||
}
|
||||
elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#', $geometry_as_text, $aMatch))
|
||||
{
|
||||
$aPolyPoints = createPointsAroundCenter($aMatch[1], $aMatch[2], $fRadius);
|
||||
}
|
||||
|
||||
if (isset($aPolyPoints))
|
||||
{
|
||||
$aResultPoints = array();
|
||||
foreach($aPolyPoints as $aPoint)
|
||||
{
|
||||
$aResultPoints[] = array($aPoint[1], $aPoint[2]);
|
||||
}
|
||||
return $aResultPoints;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
function createPointsAroundCenter($fLon, $fLat, $fRadius)
|
||||
{
|
||||
$iSteps = max(8, min(100, ($fRadius * 40000)^2));
|
||||
$fStepSize = (2*pi())/$iSteps;
|
||||
$aPolyPoints = array();
|
||||
for($f = 0; $f < 2*pi(); $f += $fStepSize)
|
||||
{
|
||||
$aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
|
||||
$aPolyPoints[] = array('', $fLon+($fRadius*sin($f)), $fLat+($fRadius*cos($f)) );
|
||||
}
|
||||
}
|
||||
return $aPolyPoints;
|
||||
return $aPolyPoints;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,38 @@
|
||||
if (isset($aPlace['aAddress'])) $aFilteredPlaces['address'] = $aPlace['aAddress'];
|
||||
if (isset($aPlace['sExtraTags'])) $aFilteredPlaces['extratags'] = $aPlace['sExtraTags'];
|
||||
if (isset($aPlace['sNameDetails'])) $aFilteredPlaces['namedetails'] = $aPlace['sNameDetails'];
|
||||
|
||||
if (isset($aPlace['aBoundingBox']))
|
||||
{
|
||||
$aFilteredPlaces['boundingbox'] = $aPlace['aBoundingBox'];
|
||||
|
||||
if (isset($aPlace['aPolyPoints']) && $bAsPoints)
|
||||
{
|
||||
$aFilteredPlaces['polygonpoints'] = $aPlace['aPolyPoints'];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($aPlace['asgeojson']))
|
||||
{
|
||||
$aFilteredPlaces['geojson'] = json_decode($aPlace['asgeojson']);
|
||||
}
|
||||
|
||||
if (isset($aPlace['assvg']))
|
||||
{
|
||||
$aFilteredPlaces['svg'] = $aPlace['assvg'];
|
||||
}
|
||||
|
||||
if (isset($aPlace['astext']))
|
||||
{
|
||||
$aFilteredPlaces['geotext'] = $aPlace['astext'];
|
||||
}
|
||||
|
||||
if (isset($aPlace['askml']))
|
||||
{
|
||||
$aFilteredPlaces['geokml'] = $aPlace['askml'];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
javascript_renderData($aFilteredPlaces);
|
||||
|
||||
@@ -36,6 +36,37 @@
|
||||
if (isset($aPlace['aAddress'])) $aFilteredPlaces['address'] = $aPlace['aAddress'];
|
||||
if (isset($aPlace['sExtraTags'])) $aFilteredPlaces['extratags'] = $aPlace['sExtraTags'];
|
||||
if (isset($aPlace['sNameDetails'])) $aFilteredPlaces['namedetails'] = $aPlace['sNameDetails'];
|
||||
|
||||
if (isset($aPlace['aBoundingBox']))
|
||||
{
|
||||
$aFilteredPlaces['boundingbox'] = $aPlace['aBoundingBox'];
|
||||
|
||||
if (isset($aPlace['aPolyPoints']) && $bAsPoints)
|
||||
{
|
||||
$aFilteredPlaces['polygonpoints'] = $aPlace['aPolyPoints'];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($aPlace['asgeojson']))
|
||||
{
|
||||
$aFilteredPlaces['geojson'] = json_decode($aPlace['asgeojson']);
|
||||
}
|
||||
|
||||
if (isset($aPlace['assvg']))
|
||||
{
|
||||
$aFilteredPlaces['svg'] = $aPlace['assvg'];
|
||||
}
|
||||
|
||||
if (isset($aPlace['astext']))
|
||||
{
|
||||
$aFilteredPlaces['geotext'] = $aPlace['astext'];
|
||||
}
|
||||
|
||||
if (isset($aPlace['askml']))
|
||||
{
|
||||
$aFilteredPlaces['geokml'] = $aPlace['askml'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
javascript_renderData($aFilteredPlaces);
|
||||
|
||||
@@ -27,6 +27,40 @@
|
||||
if ($aPlace['ref']) echo ' ref="'.htmlspecialchars($aPlace['ref']).'"';
|
||||
if (isset($aPlace['lat'])) echo ' lat="'.htmlspecialchars($aPlace['lat']).'"';
|
||||
if (isset($aPlace['lon'])) echo ' lon="'.htmlspecialchars($aPlace['lon']).'"';
|
||||
if (isset($aPlace['aBoundingBox']))
|
||||
{
|
||||
echo ' boundingbox="';
|
||||
echo join(',', $aPlace['aBoundingBox']);
|
||||
echo '"';
|
||||
|
||||
if ($bAsPoints && isset($aPlace['aPolyPoints']))
|
||||
{
|
||||
echo ' polygonpoints=\'';
|
||||
echo json_encode($aPlace['aPolyPoints']);
|
||||
echo '\'';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($aPlace['asgeojson']))
|
||||
{
|
||||
echo ' geojson=\'';
|
||||
echo $aPlace['asgeojson'];
|
||||
echo '\'';
|
||||
}
|
||||
|
||||
if (isset($aPlace['assvg']))
|
||||
{
|
||||
echo ' geosvg=\'';
|
||||
echo $aPlace['assvg'];
|
||||
echo '\'';
|
||||
}
|
||||
|
||||
if (isset($aPlace['astext']))
|
||||
{
|
||||
echo ' geotext=\'';
|
||||
echo $aPlace['astext'];
|
||||
echo '\'';
|
||||
}
|
||||
echo ">".htmlspecialchars($aPlace['langaddress'])."</result>";
|
||||
|
||||
if (isset($aPlace['aAddress']))
|
||||
@@ -63,6 +97,14 @@
|
||||
}
|
||||
echo "</namedetails>";
|
||||
}
|
||||
|
||||
if (isset($aPlace['askml']))
|
||||
{
|
||||
echo "\n<geokml>";
|
||||
echo $aPlace['askml'];
|
||||
echo "</geokml>";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
echo "</reversegeocode>";
|
||||
|
||||
@@ -18,11 +18,7 @@
|
||||
|
||||
if (isset($aPointDetails['aBoundingBox']))
|
||||
{
|
||||
$aPlace['boundingbox'] = array(
|
||||
$aPointDetails['aBoundingBox'][0],
|
||||
$aPointDetails['aBoundingBox'][1],
|
||||
$aPointDetails['aBoundingBox'][2],
|
||||
$aPointDetails['aBoundingBox'][3]);
|
||||
$aPlace['boundingbox'] = $aPointDetails['aBoundingBox'][0];
|
||||
|
||||
if (isset($aPointDetails['aPolyPoints']) && $bShowPolygons)
|
||||
{
|
||||
|
||||
@@ -16,11 +16,7 @@
|
||||
|
||||
if (isset($aPointDetails['aBoundingBox']))
|
||||
{
|
||||
$aPlace['boundingbox'] = array(
|
||||
$aPointDetails['aBoundingBox'][0],
|
||||
$aPointDetails['aBoundingBox'][1],
|
||||
$aPointDetails['aBoundingBox'][2],
|
||||
$aPointDetails['aBoundingBox'][3]);
|
||||
$aPlace['boundingbox'] = $aPointDetails['aBoundingBox'];
|
||||
|
||||
if (isset($aPointDetails['aPolyPoints']) && $bShowPolygons)
|
||||
{
|
||||
|
||||
@@ -36,10 +36,7 @@
|
||||
if (isset($aResult['aBoundingBox']))
|
||||
{
|
||||
echo ' boundingbox="';
|
||||
echo $aResult['aBoundingBox'][0];
|
||||
echo ','.$aResult['aBoundingBox'][1];
|
||||
echo ','.$aResult['aBoundingBox'][2];
|
||||
echo ','.$aResult['aBoundingBox'][3];
|
||||
echo join(',',$aResult['aBoundingBox']);
|
||||
echo '"';
|
||||
|
||||
if ($bShowPolygons && isset($aResult['aPolyPoints']))
|
||||
|
||||
@@ -12,6 +12,51 @@ class NominatimTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
|
||||
|
||||
public function test_getClassTypesWithImportance()
|
||||
{
|
||||
$aClasses = getClassTypesWithImportance();
|
||||
|
||||
$this->assertGreaterThan(
|
||||
200,
|
||||
count($aClasses)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
array(
|
||||
'label' => "Country",
|
||||
'frequency' => 0,
|
||||
'icon' => "poi_boundary_administrative",
|
||||
'defzoom' => 6,
|
||||
'defdiameter' => 15,
|
||||
'importance' => 3
|
||||
),
|
||||
$aClasses['place:country']
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function test_getResultDiameter()
|
||||
{
|
||||
$aResult = array();
|
||||
$this->assertEquals(
|
||||
0.0001,
|
||||
getResultDiameter($aResult)
|
||||
);
|
||||
|
||||
$aResult = array('class' => 'place', 'type' => 'country');
|
||||
$this->assertEquals(
|
||||
15,
|
||||
getResultDiameter($aResult)
|
||||
);
|
||||
|
||||
$aResult = array('class' => 'boundary', 'type' => 'administrative', 'admin_level' => 6);
|
||||
$this->assertEquals(
|
||||
0.32,
|
||||
getResultDiameter($aResult)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function test_addQuotes()
|
||||
{
|
||||
// FIXME: not quoting existing quote signs is probably a bug
|
||||
@@ -142,36 +187,48 @@ class NominatimTest extends \PHPUnit_Framework_TestCase
|
||||
65536,
|
||||
count( getWordSets(array_fill( 0, 18, 'a'),0) )
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
// you might say we're creating a circle
|
||||
public function test_createPointsAroundCenter()
|
||||
{
|
||||
$aPoints = createPointsAroundCenter(0, 0, 2);
|
||||
|
||||
$this->assertEquals(
|
||||
101,
|
||||
count($aPoints)
|
||||
);
|
||||
$this->assertEquals(
|
||||
array(
|
||||
['', 0, 2],
|
||||
['', 0.12558103905863, 1.9960534568565],
|
||||
['', 0.25066646712861, 1.984229402629]
|
||||
),
|
||||
array_splice($aPoints, 0, 3)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_geometryText2Points()
|
||||
{
|
||||
$fRadius = 1;
|
||||
|
||||
// invalid value
|
||||
$this->assertEquals(
|
||||
NULL,
|
||||
geometryText2Points('', $fRadius)
|
||||
);
|
||||
|
||||
|
||||
// POINT
|
||||
$aPoints = geometryText2Points('POINT(10 20)', $fRadius);
|
||||
$this->assertEquals(
|
||||
101,
|
||||
count($aPoints)
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
|
||||
array(
|
||||
['', 10, 21],
|
||||
['', 10.062790519529, 20.998026728428],
|
||||
['', 10.125333233564, 20.992114701314]
|
||||
array(
|
||||
[10, 21],
|
||||
[10.062790519529, 20.998026728428],
|
||||
[10.125333233564, 20.992114701314]
|
||||
),
|
||||
array_splice($aPoints, 0,3)
|
||||
);
|
||||
@@ -179,35 +236,25 @@ class NominatimTest extends \PHPUnit_Framework_TestCase
|
||||
// POLYGON
|
||||
$this->assertEquals(
|
||||
array(
|
||||
['30 10', '30', '10'],
|
||||
['40 40', '40', '40'],
|
||||
['20 40', '20', '40'],
|
||||
['10 20', '10', '20'],
|
||||
['30 10', '30', '10']
|
||||
['30', '10'],
|
||||
['40', '40'],
|
||||
['20', '40'],
|
||||
['10', '20'],
|
||||
['30', '10']
|
||||
),
|
||||
geometryText2Points('POLYGON((30 10, 40 40, 20 40, 10 20, 30 10))', $fRadius)
|
||||
);
|
||||
|
||||
// MULTIPOLYGON
|
||||
// only the first polygon is used
|
||||
$this->assertEquals(
|
||||
array(
|
||||
['30 20', '30', '20'],
|
||||
['45 40', '45', '40'],
|
||||
['10 40', '10', '40'],
|
||||
['30 20', '30', '20'],
|
||||
|
||||
// ['15 5' , '15', '5' ],
|
||||
// ['45 10', '45', '10'],
|
||||
// ['10 20', '10', '20'],
|
||||
// ['5 10' , '5' , '10'],
|
||||
// ['15 5' , '15', '5' ]
|
||||
['30', '20'], // first polygon only
|
||||
['45', '40'],
|
||||
['10', '40'],
|
||||
['30', '20'],
|
||||
),
|
||||
geometryText2Points('MULTIPOLYGON(((30 20, 45 40, 10 40, 30 20)),((15 5, 40 10, 10 20, 5 10, 15 5)))', $fRadius)
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -61,3 +61,75 @@ Feature: Reverse geocoding
|
||||
| xml
|
||||
| json
|
||||
| jsonv2
|
||||
|
||||
|
||||
Scenario Outline: Reverse Geocoding contains TEXT geometry
|
||||
Given the request parameters
|
||||
| polygon_text
|
||||
| 1
|
||||
When looking up <format> coordinates 48.86093,2.2978
|
||||
Then result 0 has attributes <response_attribute>
|
||||
|
||||
Examples:
|
||||
| format | response_attribute
|
||||
| xml | geotext
|
||||
| json | geotext
|
||||
| jsonv2 | geotext
|
||||
|
||||
Scenario Outline: Reverse Geocoding contains polygon-as-points geometry
|
||||
Given the request parameters
|
||||
| polygon
|
||||
| 1
|
||||
When looking up <format> coordinates 48.86093,2.2978
|
||||
Then result 0 has not attributes <response_attribute>
|
||||
|
||||
Examples:
|
||||
| format | response_attribute
|
||||
| xml | polygonpoints
|
||||
| json | polygonpoints
|
||||
| jsonv2 | polygonpoints
|
||||
|
||||
|
||||
|
||||
Scenario Outline: Reverse Geocoding contains SVG geometry
|
||||
Given the request parameters
|
||||
| polygon_svg
|
||||
| 1
|
||||
When looking up <format> coordinates 48.86093,2.2978
|
||||
Then result 0 has attributes <response_attribute>
|
||||
|
||||
Examples:
|
||||
| format | response_attribute
|
||||
| xml | geosvg
|
||||
| json | svg
|
||||
| jsonv2 | svg
|
||||
|
||||
|
||||
Scenario Outline: Reverse Geocoding contains KML geometry
|
||||
Given the request parameters
|
||||
| polygon_kml
|
||||
| 1
|
||||
When looking up <format> coordinates 48.86093,2.2978
|
||||
Then result 0 has attributes <response_attribute>
|
||||
|
||||
Examples:
|
||||
| format | response_attribute
|
||||
| xml | geokml
|
||||
| json | geokml
|
||||
| jsonv2 | geokml
|
||||
|
||||
|
||||
Scenario Outline: Reverse Geocoding contains GEOJSON geometry
|
||||
Given the request parameters
|
||||
| polygon_geojson
|
||||
| 1
|
||||
When looking up <format> coordinates 48.86093,2.2978
|
||||
Then result 0 has attributes <response_attribute>
|
||||
|
||||
Examples:
|
||||
| format | response_attribute
|
||||
| xml | geojson
|
||||
| json | geojson
|
||||
| jsonv2 | geojson
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,49 @@ Feature: Simple Reverse Tests
|
||||
| -79.34 | 23.5
|
||||
| 0.23 | -178.555
|
||||
|
||||
Scenario Outline: Testing different parameters
|
||||
Given the request parameters
|
||||
| <parameter>
|
||||
| <value>
|
||||
When sending search query "Manchester"
|
||||
Then the result is valid html
|
||||
Given the request parameters
|
||||
| <parameter>
|
||||
| <value>
|
||||
When sending html search query "Manchester"
|
||||
Then the result is valid html
|
||||
Given the request parameters
|
||||
| <parameter>
|
||||
| <value>
|
||||
When sending xml search query "Manchester"
|
||||
Then the result is valid xml
|
||||
Given the request parameters
|
||||
| <parameter>
|
||||
| <value>
|
||||
When sending json search query "Manchester"
|
||||
Then the result is valid json
|
||||
Given the request parameters
|
||||
| <parameter>
|
||||
| <value>
|
||||
When sending jsonv2 search query "Manchester"
|
||||
Then the result is valid json
|
||||
|
||||
Examples:
|
||||
| parameter | value
|
||||
| polygon | 1
|
||||
| polygon | 0
|
||||
| polygon_text | 1
|
||||
| polygon_text | 0
|
||||
| polygon_kml | 1
|
||||
| polygon_kml | 0
|
||||
| polygon_geojson | 1
|
||||
| polygon_geojson | 0
|
||||
| polygon_svg | 1
|
||||
| polygon_svg | 0
|
||||
|
||||
|
||||
|
||||
|
||||
Scenario Outline: Wrapping of legal jsonp requests
|
||||
Given the request parameters
|
||||
| json_callback
|
||||
|
||||
@@ -230,3 +230,73 @@ Feature: Search queries
|
||||
| xml
|
||||
| json
|
||||
| jsonv2
|
||||
|
||||
|
||||
Scenario Outline: Search result with contains TEXT geometry
|
||||
Given the request parameters
|
||||
| polygon_text
|
||||
| 1
|
||||
When sending <format> search query "switzerland"
|
||||
Then result 0 has attributes <response_attribute>
|
||||
|
||||
Examples:
|
||||
| format | response_attribute
|
||||
| xml | geotext
|
||||
| json | geotext
|
||||
| jsonv2 | geotext
|
||||
|
||||
Scenario Outline: Search result contains polygon-as-points geometry
|
||||
Given the request parameters
|
||||
| polygon
|
||||
| 1
|
||||
When sending <format> search query "switzerland"
|
||||
Then result 0 has attributes <response_attribute>
|
||||
|
||||
Examples:
|
||||
| format | response_attribute
|
||||
| xml | polygonpoints
|
||||
| json | polygonpoints
|
||||
| jsonv2 | polygonpoints
|
||||
|
||||
|
||||
|
||||
Scenario Outline: Search result contains SVG geometry
|
||||
Given the request parameters
|
||||
| polygon_svg
|
||||
| 1
|
||||
When sending <format> search query "switzerland"
|
||||
Then result 0 has attributes <response_attribute>
|
||||
|
||||
Examples:
|
||||
| format | response_attribute
|
||||
| xml | geosvg
|
||||
| json | svg
|
||||
| jsonv2 | svg
|
||||
|
||||
|
||||
Scenario Outline: Search result contains KML geometry
|
||||
Given the request parameters
|
||||
| polygon_kml
|
||||
| 1
|
||||
When sending <format> search query "switzerland"
|
||||
Then result 0 has attributes <response_attribute>
|
||||
|
||||
Examples:
|
||||
| format | response_attribute
|
||||
| xml | geokml
|
||||
| json | geokml
|
||||
| jsonv2 | geokml
|
||||
|
||||
|
||||
Scenario Outline: Search result contains GEOJSON geometry
|
||||
Given the request parameters
|
||||
| polygon_geojson
|
||||
| 1
|
||||
When sending <format> search query "switzerland"
|
||||
Then result 0 has attributes <response_attribute>
|
||||
|
||||
Examples:
|
||||
| format | response_attribute
|
||||
| xml | geojson
|
||||
| json | geojson
|
||||
| jsonv2 | geojson
|
||||
|
||||
@@ -98,6 +98,8 @@ def _parse_xml():
|
||||
attrs = dict(tag.attributes.items())
|
||||
assert_in('desc', attrs)
|
||||
world.results[0]['namedetails'][attrs['desc']] = tag.firstChild.nodeValue.strip()
|
||||
elif node.nodeName == "geokml":
|
||||
world.results[0]['geokml'] = node
|
||||
elif node.nodeName == "#text":
|
||||
pass
|
||||
else:
|
||||
|
||||
@@ -18,6 +18,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$bAsPoints = false;
|
||||
$bAsGeoJSON = (boolean)isset($_GET['polygon_geojson']) && $_GET['polygon_geojson'];
|
||||
$bAsKML = (boolean)isset($_GET['polygon_kml']) && $_GET['polygon_kml'];
|
||||
$bAsSVG = (boolean)isset($_GET['polygon_svg']) && $_GET['polygon_svg'];
|
||||
$bAsText = (boolean)isset($_GET['polygon_text']) && $_GET['polygon_text'];
|
||||
if ( ( ($bAsGeoJSON?1:0)
|
||||
+ ($bAsKML?1:0)
|
||||
+ ($bAsSVG?1:0)
|
||||
+ ($bAsText?1:0)
|
||||
+ ($bAsPoints?1:0)
|
||||
) > CONST_PolygonOutput_MaximumTypes)
|
||||
{
|
||||
if (CONST_PolygonOutput_MaximumTypes)
|
||||
{
|
||||
userError("Select only ".CONST_PolygonOutput_MaximumTypes." polgyon output option");
|
||||
}
|
||||
else
|
||||
{
|
||||
userError("Polygon output is disabled");
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Polygon simplification threshold (optional)
|
||||
$fThreshold = 0.0;
|
||||
if (isset($_GET['polygon_threshold'])) $fThreshold = (float)$_GET['polygon_threshold'];
|
||||
|
||||
|
||||
$oDB =& getDB();
|
||||
ini_set('memory_limit', '200M');
|
||||
|
||||
@@ -63,6 +93,22 @@
|
||||
$oPlaceLookup->setIncludeNameDetails(getParamBool('namedetails', false));
|
||||
|
||||
$aPlace = $oPlaceLookup->lookupPlace($aLookup);
|
||||
|
||||
$oPlaceLookup->setIncludePolygonAsPoints($bAsPoints);
|
||||
$oPlaceLookup->setIncludePolygonAsText($bAsText);
|
||||
$oPlaceLookup->setIncludePolygonAsGeoJSON($bAsGeoJSON);
|
||||
$oPlaceLookup->setIncludePolygonAsKML($bAsKML);
|
||||
$oPlaceLookup->setIncludePolygonAsSVG($bAsSVG);
|
||||
$oPlaceLookup->setPolygonSimplificationThreshold($fThreshold);
|
||||
|
||||
$fRadius = $fDiameter = getResultDiameter($aPlace);
|
||||
$aOutlineResult = $oPlaceLookup->getOutlines($aPlace['place_id'],$aPlace['lon'],$aPlace['lat'],$fRadius);
|
||||
|
||||
foreach($aOutlineResult as $k => $v)
|
||||
{
|
||||
$aPlace[$k] = $v;
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user