diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Maps/src |
first commit
Diffstat (limited to 'www/wiki/extensions/Maps/src')
60 files changed, 6582 insertions, 0 deletions
diff --git a/www/wiki/extensions/Maps/src/DataAccess/CachingGeocoder.php b/www/wiki/extensions/Maps/src/DataAccess/CachingGeocoder.php new file mode 100644 index 00000000..b27bbc92 --- /dev/null +++ b/www/wiki/extensions/Maps/src/DataAccess/CachingGeocoder.php @@ -0,0 +1,46 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\DataAccess; + +use BagOStuff; +use DataValues\Geo\Values\LatLongValue; +use Jeroen\SimpleGeocoder\Geocoder; + +/** + * @since 5.0 + * + * @licence GNU GPL v2+ + * @author HgO < hgo@batato.be > + */ +class CachingGeocoder implements Geocoder { + + private $geocoder; + private $cache; + private $cacheTtl; + + public function __construct( Geocoder $geocoder, BagOStuff $cache, int $cacheTtl ) { + $this->geocoder = $geocoder; + $this->cache = $cache; + $this->cacheTtl = $cacheTtl; + } + + /** + * @return LatLongValue|null + */ + public function geocode( string $address ) { + $key = $this->cache->makeKey( __CLASS__, $address ); + + $coordinates = $this->cache->get( $key ); + + // There was no entry in the cache, so we retrieve the coordinates + if ( $coordinates === false ) { + $coordinates = $this->geocoder->geocode( $address ); + + $this->cache->set( $key, $coordinates, $this->cacheTtl ); + } + + return $coordinates; + } +} diff --git a/www/wiki/extensions/Maps/src/DataAccess/JsonFileParser.php b/www/wiki/extensions/Maps/src/DataAccess/JsonFileParser.php new file mode 100644 index 00000000..81d6cfa0 --- /dev/null +++ b/www/wiki/extensions/Maps/src/DataAccess/JsonFileParser.php @@ -0,0 +1,79 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\DataAccess; + +use FileFetcher\FileFetcher; +use FileFetcher\FileFetchingException; +use Maps\MapsFactory; +use ValueParsers\ParseException; +use ValueParsers\ValueParser; + +/** + * Returns the content of the JSON file at the specified location as array. + * Empty array is returned on failure. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class JsonFileParser implements ValueParser { + + private $fileFetcher; + private $pageContentFetcher; + private $defaultNamespace; + + public function __construct( $fileFetcher = null, PageContentFetcher $pageContentFetcher = null ) { + $this->fileFetcher = $fileFetcher instanceof FileFetcher + ? $fileFetcher : MapsFactory::newDefault()->getGeoJsonFileFetcher(); + + $this->pageContentFetcher = $pageContentFetcher instanceof PageContentFetcher + ? $pageContentFetcher : MapsFactory::newDefault()->getPageContentFetcher(); + + $this->defaultNamespace = NS_GEO_JSON; + } + + /** + * @param string $fileLocation + * + * @return array + * @throws ParseException + */ + public function parse( $fileLocation ) { + $jsonString = $this->getJsonString( $fileLocation ); + + if ( $jsonString === null ) { + return []; + } + + $json = json_decode( $jsonString, true ); + + if ( $json === null ) { + return []; + } + + return $json; + } + + private function getJsonString( string $fileLocation ): ?string { + $content = $this->pageContentFetcher->getPageContent( $fileLocation, $this->defaultNamespace ); + + if ( $content instanceof \JsonContent ) { + return $content->getNativeData(); + } + + // Prevent reading JSON files on the server + if( !filter_var( $fileLocation, FILTER_VALIDATE_URL) ) { + return null; + } + + try { + return $this->fileFetcher->fetchFile( $fileLocation ); + } + catch ( FileFetchingException $ex ) { + return null; + } + } + + +} diff --git a/www/wiki/extensions/Maps/src/DataAccess/MapsFileFetcher.php b/www/wiki/extensions/Maps/src/DataAccess/MapsFileFetcher.php new file mode 100644 index 00000000..79d7f07f --- /dev/null +++ b/www/wiki/extensions/Maps/src/DataAccess/MapsFileFetcher.php @@ -0,0 +1,26 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\DataAccess; + +use FileFetcher\FileFetcher; +use FileFetcher\FileFetchingException; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class MapsFileFetcher implements FileFetcher { + + public function fetchFile( string $fileUrl ): string { + $result = \Http::get( $fileUrl ); + + if ( !is_string( $result ) ) { + throw new FileFetchingException( $fileUrl ); + } + + return $result; + } + +} diff --git a/www/wiki/extensions/Maps/src/DataAccess/MediaWikiFileUrlFinder.php b/www/wiki/extensions/Maps/src/DataAccess/MediaWikiFileUrlFinder.php new file mode 100644 index 00000000..aabb5f38 --- /dev/null +++ b/www/wiki/extensions/Maps/src/DataAccess/MediaWikiFileUrlFinder.php @@ -0,0 +1,31 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\DataAccess; + +use ImagePage; +use Maps\FileUrlFinder; +use Title; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class MediaWikiFileUrlFinder implements FileUrlFinder { + + public function getUrlForFileName( string $fileName ): string { + $colonPosition = strpos( $fileName, ':' ); + + $titleWithoutPrefix = $colonPosition === false ? $fileName : substr( $fileName, $colonPosition + 1 ); + + $title = Title::newFromText( trim( $titleWithoutPrefix ), NS_FILE ); + + if ( $title !== null && $title->exists() ) { + return ( new ImagePage( $title ) )->getDisplayedFile()->getURL(); + } + + return trim( $fileName ); + } + +} diff --git a/www/wiki/extensions/Maps/src/DataAccess/PageContentFetcher.php b/www/wiki/extensions/Maps/src/DataAccess/PageContentFetcher.php new file mode 100644 index 00000000..514cfe0a --- /dev/null +++ b/www/wiki/extensions/Maps/src/DataAccess/PageContentFetcher.php @@ -0,0 +1,40 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\DataAccess; + +use MediaWiki\Storage\RevisionLookup; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class PageContentFetcher { + + private $titleParser; + private $revisionLookup; + + public function __construct( \TitleParser $titleParser, RevisionLookup $revisionLookup ) { + $this->titleParser = $titleParser; + $this->revisionLookup = $revisionLookup; + } + + public function getPageContent( string $pageTitle, int $defaultNamespace = NS_MAIN ): ?\Content { + try { + $title = $this->titleParser->parseTitle( $pageTitle, $defaultNamespace ); + } + catch ( \MalformedTitleException $e ) { + return null; + } + + $revision = $this->revisionLookup->getRevisionByTitle( $title ); + + if ( $revision === null ) { + return null; + } + + return $revision->getContent( 'main' ); + } + +} diff --git a/www/wiki/extensions/Maps/src/Elements/BaseElement.php b/www/wiki/extensions/Maps/src/Elements/BaseElement.php new file mode 100644 index 00000000..2e36c3b4 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Elements/BaseElement.php @@ -0,0 +1,57 @@ +<?php + +namespace Maps\Elements; + +/** + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Kim Eik < kim@heldig.org > + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +abstract class BaseElement { + + private $title; + private $text; + private $link; + + public function setTitle( string $title ) { + $this->title = trim( $title ); + } + + public function setText( string $text ) { + $this->text = trim( $text ); + } + + public function setLink( string $link ) { + $this->link = $link; + } + + public function getArrayValue() { + return $this->getJSONObject(); + } + + /** + * @deprecated + */ + public function getJSONObject( string $defText = '', string $defTitle = '' ): array { + return [ + 'text' => $this->text ?? $defText, + 'title' => $this->title ?? $defTitle, + 'link' => $this->link ?? '', + ]; + } + + public function getText(): string { + return $this->text ?? ''; + } + + public function getTitle(): string { + return $this->title ?? ''; + } + + public function getLink(): string { + return $this->link ?? ''; + } + +} diff --git a/www/wiki/extensions/Maps/src/Elements/BaseFillableElement.php b/www/wiki/extensions/Maps/src/Elements/BaseFillableElement.php new file mode 100644 index 00000000..cf740fc8 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Elements/BaseFillableElement.php @@ -0,0 +1,45 @@ +<?php + +namespace Maps\Elements; + +/** + * @since 2.0 + */ +class BaseFillableElement extends BaseStrokableElement { + + protected $fillColor; + protected $fillOpacity; + + public function getJSONObject( string $defText = '', string $defTitle = '' ): array { + $parentArray = parent::getJSONObject( $defText, $defTitle ); + $array = [ + 'fillColor' => $this->hasFillColor() ? $this->getFillColor() : '#FF0000', + 'fillOpacity' => $this->hasFillOpacity() ? $this->getFillOpacity() : '0.5', + ]; + return array_merge( $parentArray, $array ); + } + + public function hasFillColor() { + return !is_null( $this->fillColor ) && $this->fillColor !== ''; + } + + public function getFillColor() { + return $this->fillColor; + } + + public function setFillColor( $fillColor ) { + $this->fillColor = trim( $fillColor ); + } + + public function hasFillOpacity() { + return !is_null( $this->fillOpacity ) && $this->fillOpacity !== ''; + } + + public function getFillOpacity() { + return $this->fillOpacity; + } + + public function setFillOpacity( $fillOpacity ) { + $this->fillOpacity = trim( $fillOpacity ); + } +} diff --git a/www/wiki/extensions/Maps/src/Elements/BaseStrokableElement.php b/www/wiki/extensions/Maps/src/Elements/BaseStrokableElement.php new file mode 100644 index 00000000..79befa2f --- /dev/null +++ b/www/wiki/extensions/Maps/src/Elements/BaseStrokableElement.php @@ -0,0 +1,63 @@ +<?php + +namespace Maps\Elements; + +/** + * @since 2.0 + * + * @licence GNU GPL v2+ + * @author Kim Eik < kim@heldig.org > + */ +class BaseStrokableElement extends BaseElement { + + protected $strokeColor; + protected $strokeOpacity; + protected $strokeWeight; + + public function getJSONObject( string $defText = '', string $defTitle = '' ): array { + $parentArray = parent::getJSONObject( $defText, $defTitle ); + $array = [ + 'strokeColor' => $this->hasStrokeColor() ? $this->getStrokeColor() : '#FF0000', + 'strokeOpacity' => $this->hasStrokeOpacity() ? $this->getStrokeOpacity() : '1', + 'strokeWeight' => $this->hasStrokeWeight() ? $this->getStrokeWeight() : '2' + ]; + return array_merge( $parentArray, $array ); + } + + public function hasStrokeColor() { + return !is_null( $this->strokeColor ) && $this->strokeColor !== ''; + } + + public function getStrokeColor() { + return $this->strokeColor; + } + + public function setStrokeColor( $strokeColor ) { + $this->strokeColor = trim( $strokeColor ); + } + + public function hasStrokeOpacity() { + return !is_null( $this->strokeOpacity ) && $this->strokeOpacity !== ''; + } + + public function getStrokeOpacity() { + return $this->strokeOpacity; + } + + public function setStrokeOpacity( $strokeOpacity ) { + $this->strokeOpacity = trim( $strokeOpacity ); + } + + public function hasStrokeWeight() { + return !is_null( $this->strokeWeight ) && $this->strokeWeight !== ''; + } + + public function getStrokeWeight() { + return $this->strokeWeight; + } + + public function setStrokeWeight( $strokeWeight ) { + $this->strokeWeight = trim( $strokeWeight ); + } + +} diff --git a/www/wiki/extensions/Maps/src/Elements/Circle.php b/www/wiki/extensions/Maps/src/Elements/Circle.php new file mode 100644 index 00000000..5b5f9172 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Elements/Circle.php @@ -0,0 +1,62 @@ +<?php + +namespace Maps\Elements; + +use DataValues\Geo\Values\LatLongValue; +use InvalidArgumentException; + +/** + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Kim Eik < kim@heldig.org > + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class Circle extends \Maps\Elements\BaseFillableElement { + + private $circleCentre; + private $circleRadius; + + public function __construct( LatLongValue $circleCentre, float $circleRadius ) { + if ( !is_float( $circleRadius ) && !is_int( $circleRadius ) ) { + throw new InvalidArgumentException( '$circleRadius must be a float or int' ); + } + + if ( $circleRadius <= 0 ) { + throw new InvalidArgumentException( '$circleRadius must be greater than zero' ); + } + + $this->setCircleCentre( $circleCentre ); + $this->setCircleRadius( $circleRadius ); + } + + public function getJSONObject( string $defText = '', string $defTitle = '' ): array { + return array_merge( + parent::getJSONObject( $defText, $defTitle ), + [ + 'centre' => [ + 'lon' => $this->getCircleCentre()->getLongitude(), + 'lat' => $this->getCircleCentre()->getLatitude() + ], + 'radius' => intval( $this->getCircleRadius() ), + ] + ); + } + + public function getCircleCentre(): LatLongValue { + return $this->circleCentre; + } + + public function setCircleCentre( LatLongValue $circleCentre ) { + $this->circleCentre = $circleCentre; + } + + public function getCircleRadius(): float { + return $this->circleRadius; + } + + public function setCircleRadius( float $circleRadius ) { + $this->circleRadius = $circleRadius; + } + +} diff --git a/www/wiki/extensions/Maps/src/Elements/ImageOverlay.php b/www/wiki/extensions/Maps/src/Elements/ImageOverlay.php new file mode 100644 index 00000000..4e48f098 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Elements/ImageOverlay.php @@ -0,0 +1,36 @@ +<?php + +namespace Maps\Elements; + +use DataValues\Geo\Values\LatLongValue; + +/** + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Kim Eik < kim@heldig.org > + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class ImageOverlay extends Rectangle { + + private $imageUrl; + + public function __construct( LatLongValue $boundsNorthEast, LatLongValue $boundsSouthWest, string $image ) { + parent::__construct( $boundsNorthEast, $boundsSouthWest ); + + $this->imageUrl = $image; + } + + public function getImage(): string { + return $this->imageUrl; + } + + public function getJSONObject( string $defText = '', string $defTitle = '' ): array { + $data = parent::getJSONObject( $defText, $defTitle ); + + $data['image'] = $this->imageUrl; + + return $data; + } + +} diff --git a/www/wiki/extensions/Maps/src/Elements/Line.php b/www/wiki/extensions/Maps/src/Elements/Line.php new file mode 100644 index 00000000..2ea4ccda --- /dev/null +++ b/www/wiki/extensions/Maps/src/Elements/Line.php @@ -0,0 +1,69 @@ +<?php + +namespace Maps\Elements; + +use DataValues\Geo\Values\LatLongValue; +use InvalidArgumentException; + +/** + * Class representing a collection of LatLongValue objects forming a line. + * + * @since 3.0 + * + * + * @licence GNU GPL v2+ + * @author Kim Eik < kim@heldig.org > + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class Line extends \Maps\Elements\BaseStrokableElement { + + /** + * @since 3.0 + * + * @var LatLongValue[] + */ + protected $coordinates; + + /** + * @since 3.0 + * + * @param LatLongValue[] $coordinates + * + * @throws InvalidArgumentException + */ + public function __construct( array $coordinates = [] ) { + foreach ( $coordinates as $coordinate ) { + if ( !( $coordinate instanceof LatLongValue ) ) { + throw new InvalidArgumentException( 'Can only construct Line with LatLongValue objects' ); + } + } + + $this->coordinates = $coordinates; + } + + /** + * @since 3.0 + * + * @return LatLongValue[] + */ + public function getLineCoordinates() { + return $this->coordinates; + } + + public function getJSONObject( string $defText = '', string $defTitle = '' ): array { + $parentArray = parent::getJSONObject( $defText, $defTitle ); + $posArray = []; + + foreach ( $this->coordinates as $mapLocation ) { + $posArray[] = [ + 'lat' => $mapLocation->getLatitude(), + 'lon' => $mapLocation->getLongitude() + ]; + } + + $posArray = [ 'pos' => $posArray ]; + + return array_merge( $parentArray, $posArray ); + } + +} diff --git a/www/wiki/extensions/Maps/src/Elements/Location.php b/www/wiki/extensions/Maps/src/Elements/Location.php new file mode 100644 index 00000000..6ea8c4be --- /dev/null +++ b/www/wiki/extensions/Maps/src/Elements/Location.php @@ -0,0 +1,157 @@ +<?php + +namespace Maps\Elements; + +use DataValues\Geo\Values\LatLongValue; + +/** + * Class describing a single location (geographical point). + * + * TODO: rethink the design of this class after deciding on what actual role it has + * + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @author Daniel Werner + */ +class Location extends BaseElement { + + /** + * @var LatLongValue + */ + private $coordinates; + + /** + * @var string + */ + private $address; + + /** + * @var string + */ + private $icon = ''; + + /** + * @var string + */ + private $group = ''; + + /** + * @var string + */ + private $inlineLabel = ''; + + /** + * @var string + */ + private $visitedIcon = ''; + + public function __construct( LatLongValue $coordinates, string $title = '', string $text = '' ) { + $this->coordinates = $coordinates; + $this->setTitle( $title ); + $this->setText( $text ); + } + + public static function newFromLatLon( float $lat, float $lon ): self { + return new self( new LatLongValue( $lat, $lon ) ); + } + + public function getCoordinates(): LatLongValue { + return $this->coordinates; + } + + public function getJSONObject( string $defText = '', string $defTitle = '', string $defIconUrl = '', + string $defGroup = '', string $defInlineLabel = '', string $defVisitedIcon = '' ): array { + + $parentArray = parent::getJSONObject( $defText, $defTitle ); + + $array = [ + 'lat' => $this->coordinates->getLatitude(), + 'lon' => $this->coordinates->getLongitude(), + 'icon' => $this->hasIcon() ? \Maps\MapsFunctions::getFileUrl( $this->getIcon() ) : $defIconUrl, + ]; + $val = $this->getAddress(); + if ( $val !== '' ) { + $array['address'] = $val; + } + $val = $this->hasGroup() ? $this->getGroup() : $defGroup; + if ( !empty( $val ) ) { + $array['group'] = $val; + } + $val = $this->hasInlineLabel() ? $this->getInlineLabel() : $defInlineLabel; + if ( !empty( $val ) ) { + $array['inlineLabel'] = $val; + } + $val = $this->hasVisitedIcon() ? $this->getVisitedIcon() : $defVisitedIcon; + if ( !empty( $val ) ) { + $array['visitedicon'] = $val; + } + + return array_merge( $parentArray, $array ); + } + + public function hasIcon(): bool { + return $this->icon !== ''; + } + + public function getIcon(): string { + return $this->icon; + } + + public function setIcon( string $icon ) { + $this->icon = $icon; + } + + /** + * Returns the address corresponding to this location. + * If there is none, and empty sting is returned. + */ + public function getAddress(): string { + if ( is_null( $this->address ) ) { + $this->address = ''; + } + + return $this->address; + } + + /** + * Returns whether Location is assigned to a group. + */ + public function hasGroup(): bool { + return $this->group !== ''; + } + + public function getGroup(): string { + return $this->group; + } + + public function setGroup( string $group ) { + $this->group = trim( $group ); + } + + public function hasInlineLabel(): bool { + return $this->inlineLabel !== ''; + } + + public function getInlineLabel(): string { + return $this->inlineLabel; + } + + public function setInlineLabel( string $label ) { + $this->inlineLabel = $label; + } + + public function hasVisitedIcon(): bool { + return $this->visitedIcon !== ''; + } + + public function getVisitedIcon(): string { + return $this->visitedIcon; + } + + public function setVisitedIcon( string $visitedIcon ) { + $this->visitedIcon = $visitedIcon; + } + +} diff --git a/www/wiki/extensions/Maps/src/Elements/Polygon.php b/www/wiki/extensions/Maps/src/Elements/Polygon.php new file mode 100644 index 00000000..bd7289ec --- /dev/null +++ b/www/wiki/extensions/Maps/src/Elements/Polygon.php @@ -0,0 +1,44 @@ +<?php + +namespace Maps\Elements; + +/** + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Kim Eik < kim@heldig.org > + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class Polygon extends Line { + + private $onlyVisibleOnHover = false; + private $fillOpacity = '0.5'; + private $fillColor = '#FF0000'; + + public function isOnlyVisibleOnHover(): bool { + return $this->onlyVisibleOnHover; + } + + public function setOnlyVisibleOnHover( bool $visible ) { + $this->onlyVisibleOnHover = $visible; + } + + public function setFillOpacity( string $fillOpacity ) { + $this->fillOpacity = $fillOpacity; + } + + public function setFillColor( string $fillColor ) { + $this->fillColor = $fillColor; + } + + public function getJSONObject( string $defText = '', string $defTitle = '' ): array { + $json = parent::getJSONObject( $defText, $defTitle ); + + $json['onlyVisibleOnHover'] = $this->onlyVisibleOnHover; + $json['fillColor'] = $this->fillColor; + $json['fillOpacity'] = $this->fillOpacity; + + return $json; + } + +} diff --git a/www/wiki/extensions/Maps/src/Elements/Rectangle.php b/www/wiki/extensions/Maps/src/Elements/Rectangle.php new file mode 100644 index 00000000..d8392f70 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Elements/Rectangle.php @@ -0,0 +1,80 @@ +<?php + +namespace Maps\Elements; + +use DataValues\Geo\Values\LatLongValue; +use InvalidArgumentException; + +/** + * @since 3.0 + * + * + * @licence GNU GPL v2+ + * @author Kim Eik < kim@heldig.org > + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class Rectangle extends \Maps\Elements\BaseFillableElement { + + /** + * @since 3.0 + * @var LatLongValue + */ + protected $rectangleNorthEast; + + /** + * @since 3.0 + * @var LatLongValue + */ + protected $rectangleSouthWest; + + /** + * @since 3.0 + * + * @param LatLongValue $rectangleNorthEast + * @param LatLongValue $rectangleSouthWest + * + * @throws InvalidArgumentException + */ + public function __construct( LatLongValue $rectangleNorthEast, LatLongValue $rectangleSouthWest ) { + if ( $rectangleNorthEast->equals( $rectangleSouthWest ) ) { + throw new InvalidArgumentException( '$rectangleNorthEast cannot be equal to $rectangleSouthWest' ); + } + + // TODO: validate bounds are correct, if not, flip + $this->setRectangleNorthEast( $rectangleNorthEast ); + $this->setRectangleSouthWest( $rectangleSouthWest ); + } + + public function getJSONObject( string $defText = '', string $defTitle = '' ): array { + $parentArray = parent::getJSONObject( $defText, $defTitle ); + $array = [ + 'ne' => [ + 'lon' => $this->getRectangleNorthEast()->getLongitude(), + 'lat' => $this->getRectangleNorthEast()->getLatitude() + ], + 'sw' => [ + 'lon' => $this->getRectangleSouthWest()->getLongitude(), + 'lat' => $this->getRectangleSouthWest()->getLatitude() + ], + ]; + + return array_merge( $parentArray, $array ); + } + + public function getRectangleNorthEast(): LatLongValue { + return $this->rectangleNorthEast; + } + + public function setRectangleNorthEast( LatLongValue $rectangleNorthEast ) { + $this->rectangleNorthEast = $rectangleNorthEast; + } + + public function getRectangleSouthWest(): LatLongValue { + return $this->rectangleSouthWest; + } + + public function setRectangleSouthWest( LatLongValue $rectangleSouthWest ) { + $this->rectangleSouthWest = $rectangleSouthWest; + } + +} diff --git a/www/wiki/extensions/Maps/src/Elements/WmsOverlay.php b/www/wiki/extensions/Maps/src/Elements/WmsOverlay.php new file mode 100644 index 00000000..fa3c95f9 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Elements/WmsOverlay.php @@ -0,0 +1,71 @@ +<?php + +namespace Maps\Elements; + +/** + * Class that holds metadata on WMS overlay layers on map + * + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Mathias Lidal < mathiaslidal@gmail.com > + */ +class WmsOverlay extends BaseElement { + + /** + * @var String Base url to WMS server + */ + private $wmsServerUrl; + + /** + * @var String WMS Layer name + */ + private $wmsLayerName; + + /** + * @var String WMS Style name (default value: 'default') + */ + private $wmsStyleName; + + public function __construct( string $wmsServerUrl, string $wmsLayerName, string $wmsStyleName = "default" ) { + $this->setWmsServerUrl( $wmsServerUrl ); + $this->setWmsLayerName( $wmsLayerName ); + $this->setWmsStyleName( $wmsStyleName ); + } + + public function getJSONObject( string $defText = '', string $defTitle = '' ): array { + $parentArray = parent::getJSONObject( $defText, $defTitle ); + + $array = [ + 'wmsServerUrl' => $this->getWmsServerUrl(), + 'wmsLayerName' => $this->getWmsLayerName(), + 'wmsStyleName' => $this->getWmsStyleName() + ]; + return array_merge( $parentArray, $array ); + } + + public function getWmsServerUrl(): string { + return $this->wmsServerUrl; + } + + public function setWmsServerUrl( string $wmsServerUrl ) { + $this->wmsServerUrl = $wmsServerUrl; + } + + public function getWmsLayerName(): string { + return $this->wmsLayerName; + } + + public function setWmsLayerName( string $wmsLayerName ) { + $this->wmsLayerName = $wmsLayerName; + } + + public function getWmsStyleName(): string { + return $this->wmsStyleName; + } + + public function setWmsStyleName( string $wmsStyleName ) { + $this->wmsStyleName = $wmsStyleName; + } + +} diff --git a/www/wiki/extensions/Maps/src/FileUrlFinder.php b/www/wiki/extensions/Maps/src/FileUrlFinder.php new file mode 100644 index 00000000..c4894d53 --- /dev/null +++ b/www/wiki/extensions/Maps/src/FileUrlFinder.php @@ -0,0 +1,18 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +interface FileUrlFinder { + + /** + * Resolves the url of images provided as wiki page; leaves others alone. + */ + public function getUrlForFileName( string $fileName ): string; + +} diff --git a/www/wiki/extensions/Maps/src/GeoFunctions.php b/www/wiki/extensions/Maps/src/GeoFunctions.php new file mode 100644 index 00000000..75a92791 --- /dev/null +++ b/www/wiki/extensions/Maps/src/GeoFunctions.php @@ -0,0 +1,99 @@ +<?php + +namespace Maps; + +use DataValues\Geo\Values\LatLongValue; + +/** + * Static class containing geographical functions. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @author Pnelnik + * @author Matěj Grabovský + */ +final class GeoFunctions { + + // The approximate radius of the earth in meters, according to http://en.wikipedia.org/wiki/Earth_radius. + private const EARTH_RADIUS = 6371000; + + /** + * Returns the geographical distance between two coordinates. + * See http://en.wikipedia.org/wiki/Geographical_distance + * + * @since 2.0 + * + * @param LatLongValue $start + * @param LatLongValue $end + * + * @return float Distance in m. + */ + public static function calculateDistance( LatLongValue $start, LatLongValue $end ) { + $northRad1 = deg2rad( $start->getLatitude() ); + $eastRad1 = deg2rad( $start->getLongitude() ); + + $cosNorth1 = cos( $northRad1 ); + $cosEast1 = cos( $eastRad1 ); + + $sinNorth1 = sin( $northRad1 ); + $sinEast1 = sin( $eastRad1 ); + + $northRad2 = deg2rad( $end->getLatitude() ); + $eastRad2 = deg2rad( $end->getLongitude() ); + + $cosNorth2 = cos( $northRad2 ); + $cosEast2 = cos( $eastRad2 ); + + $sinNorth2 = sin( $northRad2 ); + $sinEast2 = sin( $eastRad2 ); + + $term1 = $cosNorth1 * $sinEast1 - $cosNorth2 * $sinEast2; + $term2 = $cosNorth1 * $cosEast1 - $cosNorth2 * $cosEast2; + $term3 = $sinNorth1 - $sinNorth2; + + $distThruSquared = $term1 * $term1 + $term2 * $term2 + $term3 * $term3; + + $distance = 2 * self::EARTH_RADIUS * asin( sqrt( $distThruSquared ) / 2 ); + + assert( $distance >= 0 ); + + return $distance; + } + + /** + * Finds a destination given a starting location, bearing and distance. + * + * @since 2.0 + * + * @param LatLongValue $startingCoordinates + * @param float $bearing The initial bearing in degrees. + * @param float $distance The distance to travel in km. + * + * @return array The destination coordinates, as non-directional floats in an array with lat and lon keys. + */ + public static function findDestination( LatLongValue $startingCoordinates, $bearing, $distance ) { + $startingCoordinates = [ + 'lat' => deg2rad( $startingCoordinates->getLatitude() ), + 'lon' => deg2rad( $startingCoordinates->getLongitude() ), + ]; + + $radBearing = deg2rad( (float)$bearing ); + $angularDistance = $distance / self::EARTH_RADIUS; + + $lat = asin( + sin( $startingCoordinates['lat'] ) * cos( $angularDistance ) + cos( $startingCoordinates['lat'] ) * sin( + $angularDistance + ) * cos( $radBearing ) + ); + $lon = $startingCoordinates['lon'] + atan2( + sin( $radBearing ) * sin( $angularDistance ) * cos( $startingCoordinates['lat'] ), + cos( $angularDistance ) - sin( $startingCoordinates['lat'] ) * sin( $lat ) + ); + + return [ + 'lat' => rad2deg( $lat ), + 'lon' => rad2deg( $lon ) + ]; + } + +}
\ No newline at end of file diff --git a/www/wiki/extensions/Maps/src/GoogleMapsService.php b/www/wiki/extensions/Maps/src/GoogleMapsService.php new file mode 100644 index 00000000..9c1b91b9 --- /dev/null +++ b/www/wiki/extensions/Maps/src/GoogleMapsService.php @@ -0,0 +1,314 @@ +<?php + +namespace Maps; + +use Html; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @author Peter Grassberger < petertheone@gmail.com > + */ +class GoogleMapsService implements MappingService { + + /** + * Maps user input map types to the Google Maps names for the map types. + */ + private const MAP_TYPES = [ + 'normal' => 'ROADMAP', + 'roadmap' => 'ROADMAP', + 'satellite' => 'SATELLITE', + 'hybrid' => 'HYBRID', + 'terrain' => 'TERRAIN', + 'physical' => 'TERRAIN', + 'earth' => 'earth' + ]; + + private const TYPE_CONTROL_STYLES = [ + 'default' => 'DEFAULT', + 'horizontal' => 'HORIZONTAL_BAR', + 'dropdown' => 'DROPDOWN_MENU' + ]; + + private $addedDependencies = []; + + public function getName(): string { + return 'googlemaps3'; + } + + public function getAliases(): array { + return [ 'googlemaps', 'google' ]; + } + + public function hasAlias( string $alias ): bool { + return in_array( $alias, [ 'googlemaps', 'google' ] ); + } + + public function getParameterInfo(): array { + global $egMapsGMaps3Type, $egMapsGMaps3Types, $egMapsGMaps3Controls, $egMapsGMaps3Layers; + global $egMapsGMaps3DefTypeStyle, $egMapsGMaps3DefZoomStyle, $egMapsGMaps3AutoInfoWindows; + global $egMapsResizableByDefault; + + $params = []; + + $params['zoom'] = [ + 'type' => 'integer', + 'range' => [ 0, 20 ], + 'default' => $GLOBALS['egMapsGMaps3Zoom'], + 'message' => 'maps-par-zoom', + ]; + + $params['type'] = [ + 'default' => $egMapsGMaps3Type, + 'values' => self::getTypeNames(), + 'message' => 'maps-googlemaps3-par-type', + 'post-format' => function ( $value ) { + return GoogleMapsService::MAP_TYPES[strtolower( $value )]; + }, + ]; + + $params['types'] = [ + 'dependencies' => 'type', + 'default' => $egMapsGMaps3Types, + 'values' => self::getTypeNames(), + 'message' => 'maps-googlemaps3-par-types', + 'islist' => true, + 'post-format' => function ( array $value ) { + foreach ( $value as &$part ) { + $part = self::MAP_TYPES[strtolower( $part )]; + } + + return $value; + }, + ]; + + $params['layers'] = [ + 'default' => $egMapsGMaps3Layers, + 'values' => [ + 'traffic', + 'bicycling', + 'transit' + ], + 'message' => 'maps-googlemaps3-par-layers', + 'islist' => true, + ]; + + $params['controls'] = [ + 'default' => $egMapsGMaps3Controls, + 'values' => [ + 'pan', + 'zoom', + 'type', + 'scale', + 'streetview', + 'rotate' + ], + 'message' => 'maps-googlemaps3-par-controls', + 'islist' => true, + 'post-format' => function ( $value ) { + return array_map( 'strtolower', $value ); + }, + ]; + + $params['zoomstyle'] = [ + 'default' => $egMapsGMaps3DefZoomStyle, + 'values' => [ 'default', 'small', 'large' ], + 'message' => 'maps-googlemaps3-par-zoomstyle', + 'post-format' => 'strtoupper', + ]; + + $params['typestyle'] = [ + 'default' => $egMapsGMaps3DefTypeStyle, + 'values' => array_keys( self::TYPE_CONTROL_STYLES ), + 'message' => 'maps-googlemaps3-par-typestyle', + 'post-format' => function ( $value ) { + return self::TYPE_CONTROL_STYLES[strtolower( $value )]; + }, + ]; + + $params['autoinfowindows'] = [ + 'type' => 'boolean', + 'default' => $egMapsGMaps3AutoInfoWindows, + 'message' => 'maps-googlemaps3-par-autoinfowindows', + ]; + + $params['resizable'] = [ + 'type' => 'boolean', + 'default' => $egMapsResizableByDefault, + 'message' => 'maps-par-resizable', + ]; + + $params['kmlrezoom'] = [ + 'type' => 'boolean', + 'default' => $GLOBALS['egMapsRezoomForKML'], + 'message' => 'maps-googlemaps3-par-kmlrezoom', + ]; + + $params['poi'] = [ + 'type' => 'boolean', + 'default' => $GLOBALS['egMapsShowPOI'], + 'message' => 'maps-googlemaps3-par-poi', + ]; + + $params['markercluster'] = [ + 'type' => 'boolean', + 'default' => false, + 'message' => 'maps-par-markercluster', + ]; + + $params['clustergridsize'] = [ + 'type' => 'integer', + 'default' => 60, + 'message' => 'maps-googlemaps3-par-clustergridsize', + ]; + + $params['clustermaxzoom'] = [ + 'type' => 'integer', + 'default' => 20, + 'message' => 'maps-par-clustermaxzoom', + ]; + + $params['clusterzoomonclick'] = [ + 'type' => 'boolean', + 'default' => true, + 'message' => 'maps-par-clusterzoomonclick', + ]; + + $params['clusteraveragecenter'] = [ + 'type' => 'boolean', + 'default' => true, + 'message' => 'maps-googlemaps3-par-clusteraveragecenter', + ]; + + $params['clusterminsize'] = [ + 'type' => 'integer', + 'default' => 2, + 'message' => 'maps-googlemaps3-par-clusterminsize', + ]; + + $params['imageoverlays'] = [ + 'type' => 'mapsimageoverlay', + 'default' => [], + 'delimiter' => ';', + 'islist' => true, + 'message' => 'maps-googlemaps3-par-imageoverlays', + ]; + + $params['kml'] = [ + 'default' => [], + 'message' => 'maps-par-kml', + 'islist' => true, + 'post-format' => function( array $kmlFileNames ) { + return array_map( + function( string $fileName ) { + return wfExpandUrl( MapsFunctions::getFileUrl( $fileName ) ); + }, + $kmlFileNames + ); + } + ]; + + $params['gkml'] = [ + 'default' => [], + 'message' => 'maps-googlemaps3-par-gkml', + 'islist' => true, + ]; + + $params['searchmarkers'] = [ + 'default' => '', + 'message' => 'maps-par-searchmarkers', + // new CriterionSearchMarkers() FIXME + ]; + + $params['enablefullscreen'] = [ + 'type' => 'boolean', + 'default' => false, + 'message' => 'maps-par-enable-fullscreen', + ]; + + $params['scrollwheelzoom'] = [ + 'type' => 'boolean', + 'default' => false, + 'message' => 'maps-par-scrollwheelzoom', + ]; + + return $params; + } + + /** + * Returns the names of all supported map types. + */ + private function getTypeNames(): array { + return array_keys( self::MAP_TYPES ); + } + + public function newMapId(): string { + static $mapsOnThisPage = 0; + + $mapsOnThisPage++; + + return 'map_google3_' . $mapsOnThisPage; + } + + public function getResourceModules(): array { + return [ 'ext.maps.googlemaps3', 'ext.sm.googlemaps3ajax' ]; + } + + public static function getApiScript( $langCode, array $urlArgs = [] ) { + $urlArgs = array_merge( + [ + 'language' => self::getMappedLanguageCode( $langCode ) + ], + $urlArgs + ); + if ( $GLOBALS['egMapsGMaps3ApiKey'] !== '' ) { + $urlArgs['key'] = $GLOBALS['egMapsGMaps3ApiKey']; + } + if ( $GLOBALS['egMapsGMaps3ApiVersion'] !== '' ) { + $urlArgs['v'] = $GLOBALS['egMapsGMaps3ApiVersion']; + } + + return Html::linkedScript( '//maps.googleapis.com/maps/api/js?' . wfArrayToCgi( $urlArgs ) ); + } + + /** + * Maps language codes to Google Maps API v3 compatible values. + */ + private static function getMappedLanguageCode( string $code ): string { + $mappings = [ + 'en_gb' => 'en-gb',// v3 supports en_gb - but wants us to call it en-gb + 'he' => 'iw', // iw is googlish for hebrew + 'fj' => 'fil', // google does not support Fijian - use Filipino as close(?) supported relative + ]; + + if ( array_key_exists( $code, $mappings ) ) { + return $mappings[$code]; + } + + return $code; + } + + public function getDependencyHtml( array $params ): string { + $dependencies = []; + + // Only add dependencies that have not yet been added. + foreach ( $this->getDependencies() as $dependency ) { + if ( !in_array( $dependency, $this->addedDependencies ) ) { + $dependencies[] = $dependency; + $this->addedDependencies[] = $dependency; + } + } + + // If there are dependencies, put them all together in a string, otherwise return false. + return $dependencies !== [] ? implode( '', $dependencies ) : false; + } + + private function getDependencies(): array { + return [ + self::getApiScript( + is_string( $GLOBALS['egMapsGMaps3Language'] ) ? + $GLOBALS['egMapsGMaps3Language'] : $GLOBALS['egMapsGMaps3Language']->getCode() + ) + ]; + } +} diff --git a/www/wiki/extensions/Maps/src/LeafletService.php b/www/wiki/extensions/Maps/src/LeafletService.php new file mode 100644 index 00000000..0e41d670 --- /dev/null +++ b/www/wiki/extensions/Maps/src/LeafletService.php @@ -0,0 +1,184 @@ +<?php + +namespace Maps; + +use Html; + +/** + * @licence GNU GPL v2+ + */ +class LeafletService implements MappingService { + + private $addedDependencies = []; + + public function getName(): string { + return 'leaflet'; + } + + public function getAliases(): array { + return [ 'leafletmaps', 'leaflet' ]; // TODO: main name should not be in here? + } + + public function hasAlias( string $alias ): bool { + return in_array( $alias, [ 'leafletmaps', 'leaflet' ] ); + } + + public function getParameterInfo(): array { + global $GLOBALS; + + $params = []; + + $params['zoom'] = [ + 'type' => 'integer', + 'range' => [ 0, 20 ], + 'default' => false, + 'message' => 'maps-par-zoom' + ]; + + $params['defzoom'] = [ + 'type' => 'integer', + 'range' => [ 0, 20 ], + 'default' => self::getDefaultZoom(), + 'message' => 'maps-leaflet-par-defzoom' + ]; + + $params['layers'] = [ + 'aliases' => 'layer', + 'type' => 'string', + 'values' => array_keys( $GLOBALS['egMapsLeafletAvailableLayers'], true, true ), + 'default' => $GLOBALS['egMapsLeafletLayers'], + 'message' => 'maps-leaflet-par-layers', + 'islist' => true, + ]; + + $params['overlaylayers'] = [ + 'type' => 'string', + 'values' => array_keys( $GLOBALS['egMapsLeafletAvailableOverlayLayers'], true, true ), + 'default' => $GLOBALS['egMapsLeafletOverlayLayers'], + 'message' => 'maps-leaflet-par-overlaylayers', + 'islist' => true, + ]; + + $params['resizable'] = [ + 'type' => 'boolean', + 'default' => $GLOBALS['egMapsResizableByDefault'], + 'message' => 'maps-par-resizable' + ]; + + $params['enablefullscreen'] = [ + 'type' => 'boolean', + 'default' => false, + 'message' => 'maps-par-enable-fullscreen', + ]; + + $params['scrollwheelzoom'] = [ + 'type' => 'boolean', + 'default' => true, + 'message' => 'maps-par-scrollwheelzoom', + ]; + + $params['markercluster'] = [ + 'type' => 'boolean', + 'default' => false, + 'message' => 'maps-par-markercluster', + ]; + + $params['clustermaxzoom'] = [ + 'type' => 'integer', + 'default' => 20, + 'message' => 'maps-par-clustermaxzoom', + ]; + + $params['clusterzoomonclick'] = [ + 'type' => 'boolean', + 'default' => true, + 'message' => 'maps-par-clusterzoomonclick', + ]; + + $params['clustermaxradius'] = [ + 'type' => 'integer', + 'default' => 80, + 'message' => 'maps-par-maxclusterradius', + ]; + + $params['clusterspiderfy'] = [ + 'type' => 'boolean', + 'default' => true, + 'message' => 'maps-leaflet-par-clusterspiderfy', + ]; + + $params['geojson'] = [ + 'type' => 'jsonfile', + 'default' => '', + 'message' => 'maps-displaymap-par-geojson', + ]; + + return $params; + } + + /** + * @since 3.0 + */ + public function getDefaultZoom() { + return $GLOBALS['egMapsLeafletZoom']; + } + + public function newMapId(): string { + static $mapsOnThisPage = 0; + + $mapsOnThisPage++; + + return 'map_leaflet_' . $mapsOnThisPage; + } + + public function getResourceModules(): array { + return [ 'ext.maps.leaflet', 'ext.sm.leafletajax' ]; + } + + public function getDependencyHtml( array $params ): string { + $dependencies = []; + + // Only add dependencies that have not yet been added. + foreach ( $this->getDependencies( $params ) as $dependency ) { + if ( !in_array( $dependency, $this->addedDependencies ) ) { + $dependencies[] = $dependency; + $this->addedDependencies[] = $dependency; + } + } + + // If there are dependencies, put them all together in a string, otherwise return false. + return $dependencies !== [] ? implode( '', $dependencies ) : false; + } + + private function getDependencies( array $params ): array { + $leafletPath = $GLOBALS['wgScriptPath'] . '/extensions/Maps/resources/leaflet/leaflet'; + + return array_merge( + [ + Html::linkedStyle( "$leafletPath/leaflet.css" ), + Html::linkedScript( "$leafletPath/leaflet.js" ), + ], + $this->getLayerDependencies( $params ) + ); + } + + private function getLayerDependencies( array $params ) { + global $egMapsLeafletLayerDependencies, $egMapsLeafletAvailableLayers, + $egMapsLeafletLayersApiKeys; + + $layerDependencies = []; + + foreach ( $params['layers'] as $layerName ) { + if ( array_key_exists( $layerName, $egMapsLeafletAvailableLayers ) + && $egMapsLeafletAvailableLayers[$layerName] + && array_key_exists( $layerName, $egMapsLeafletLayersApiKeys ) + && array_key_exists( $layerName, $egMapsLeafletLayerDependencies ) ) { + $layerDependencies[] = '<script src="' . $egMapsLeafletLayerDependencies[$layerName] . + $egMapsLeafletLayersApiKeys[$layerName] . '"></script>'; + } + } + + return array_unique( $layerDependencies ); + } + +} diff --git a/www/wiki/extensions/Maps/src/MappingService.php b/www/wiki/extensions/Maps/src/MappingService.php new file mode 100644 index 00000000..184ef712 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MappingService.php @@ -0,0 +1,33 @@ +<?php + +namespace Maps; + +use ParamProcessor\ParamDefinition; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +interface MappingService { + + public function getName(): string; + + public function getAliases(): array; + + public function hasAlias( string $alias ): bool; + + /** + * @return array[]|ParamDefinition[] + */ + public function getParameterInfo(): array; + + public function getDependencyHtml( array $params ): string; + + /** + * Returns the resource modules that need to be loaded to use this mapping service. + */ + public function getResourceModules(): array; + + public function newMapId(); + +} diff --git a/www/wiki/extensions/Maps/src/MappingServices.php b/www/wiki/extensions/Maps/src/MappingServices.php new file mode 100644 index 00000000..295a7299 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MappingServices.php @@ -0,0 +1,82 @@ +<?php + +namespace Maps; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +final class MappingServices { + + /** + * @var MappingService[] + */ + private $nameToServiceMap = []; + + /** + * @var string Name of the default service, which is used as fallback + */ + private $defaultService; + + /** + * @param string[] $availableServices + * @param string $defaultService + * @param MappingService ...$services + * @throws \InvalidArgumentException + */ + public function __construct( array $availableServices, string $defaultService, MappingService ...$services ) { + $this->defaultService = $defaultService; + + foreach ( $services as $service ) { + if ( in_array( $service->getName(), $availableServices ) ) { + $this->nameToServiceMap[$service->getName()] = $service; + + foreach ( $service->getAliases() as $alias ) { + $this->nameToServiceMap[$alias] = $service; + } + } + } + + if ( !$this->nameIsKnown( $defaultService ) ) { + throw new \InvalidArgumentException( 'The default mapping service needs to be available' ); + } + } + + /** + * @param string $name Name or alias of a service + * @return bool + */ + public function nameIsKnown( string $name ): bool { + return array_key_exists( $name, $this->nameToServiceMap ); + } + + /** + * @param string $name Name or alias of a service + * @return MappingService + * @throws \OutOfBoundsException + */ + public function getService( string $name ): MappingService { + if ( !$this->nameIsKnown( $name ) ) { + throw new \OutOfBoundsException(); + } + + return $this->nameToServiceMap[$name]; + } + + /** + * @param string $name Name or alias of a service + * @return MappingService + */ + public function getServiceOrDefault( string $name ): MappingService { + if ( $this->nameIsKnown( $name ) ) { + return $this->nameToServiceMap[$name]; + } + + return $this->nameToServiceMap[$this->defaultService]; + } + + public function getAllNames(): array { + return array_keys( $this->nameToServiceMap ); + } + +} diff --git a/www/wiki/extensions/Maps/src/MapsFactory.php b/www/wiki/extensions/Maps/src/MapsFactory.php new file mode 100644 index 00000000..6ab23d5d --- /dev/null +++ b/www/wiki/extensions/Maps/src/MapsFactory.php @@ -0,0 +1,168 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps; + +use FileFetcher\Cache\Factory as CacheFactory; +use FileFetcher\FileFetcher; +use Jeroen\SimpleGeocoder\Geocoder; +use Jeroen\SimpleGeocoder\Geocoders\Decorators\CoordinateFriendlyGeocoder; +use Jeroen\SimpleGeocoder\Geocoders\FileFetchers\GeoNamesGeocoder; +use Jeroen\SimpleGeocoder\Geocoders\FileFetchers\GoogleGeocoder; +use Jeroen\SimpleGeocoder\Geocoders\FileFetchers\NominatimGeocoder; +use Jeroen\SimpleGeocoder\Geocoders\NullGeocoder; +use Maps\DataAccess\CachingGeocoder; +use Maps\DataAccess\MapsFileFetcher; +use Maps\DataAccess\MediaWikiFileUrlFinder; +use Maps\DataAccess\PageContentFetcher; +use Maps\MediaWiki\ParserHooks\DisplayMapFunction; +use Maps\Presentation\CoordinateFormatter; +use Maps\Presentation\WikitextParsers\LocationParser; +use MediaWiki\MediaWikiServices; +use SimpleCache\Cache\Cache; +use SimpleCache\Cache\MediaWikiCache; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class MapsFactory { + + private $settings; + private $mediaWikiServices; + + private function __construct( array $settings, MediaWikiServices $mediaWikiServices ) { + $this->settings = $settings; + $this->mediaWikiServices = $mediaWikiServices; + } + + public static function newDefault(): self { + return new self( $GLOBALS, MediaWikiServices::getInstance() ); + } + + /** + * Only for legacy code where dependency injection is not possible + */ + public static function globalInstance(): self { + static $instance = null; + + if ( $instance === null ) { + $instance = self::newDefault(); + } + + return $instance; + } + + public function newLocationParser(): LocationParser { + return LocationParser::newInstance( + $this->getGeocoder(), + $this->getFileUrlFinder() + ); + } + + public function getGeocoder(): Geocoder { + $geocoder = new CoordinateFriendlyGeocoder( $this->newCoreGeocoder() ); + + if ( $this->settings['egMapsEnableGeoCache'] ) { + return new CachingGeocoder( + $geocoder, + $this->getMediaWikiCache(), + $this->settings['egMapsGeoCacheTtl'] + ); + } + + return $geocoder; + } + + private function newCoreGeocoder(): Geocoder { + switch ( $this->settings['egMapsDefaultGeoService'] ) { + case 'geonames': + if ( $this->settings['egMapsGeoNamesUser'] === '' ) { + return $this->newGoogleGeocoder(); + } + + return new GeoNamesGeocoder( + $this->newFileFetcher(), + $this->settings['egMapsGeoNamesUser'] + ); + case 'google': + return $this->newGoogleGeocoder(); + case 'nominatim': + return new NominatimGeocoder( + $this->newFileFetcher() + ); + default: + return new NullGeocoder(); + } + } + + private function newGoogleGeocoder(): Geocoder { + return new GoogleGeocoder( + $this->newFileFetcher(), + $this->settings['egMapsGMaps3ApiKey'], + $this->settings['egMapsGMaps3ApiVersion'] + ); + } + + public function getFileFetcher(): FileFetcher { + return $this->newFileFetcher(); + } + + private function newFileFetcher(): FileFetcher { + return new MapsFileFetcher(); + } + + public function getGeoJsonFileFetcher(): FileFetcher { + if ( $this->settings['egMapsGeoJsonCacheTtl'] === 0 ) { + return $this->getFileFetcher(); + } + + return ( new CacheFactory() )->newJeroenSimpleCacheFetcher( + $this->getFileFetcher(), + $this->getMediaWikiSimpleCache( $this->settings['egMapsGeoJsonCacheTtl'] ) + ); + } + + private function getMediaWikiSimpleCache( int $ttlInSeconds ): Cache { + return new MediaWikiCache( + $this->getMediaWikiCache(), + $ttlInSeconds + ); + } + + private function getMediaWikiCache(): \BagOStuff { + return wfGetCache( CACHE_ANYTHING ); + } + + public function getPageContentFetcher(): PageContentFetcher { + return new PageContentFetcher( + $this->mediaWikiServices->getTitleParser(), + $this->mediaWikiServices->getRevisionLookup() + ); + } + + public function getCoordinateFormatter(): CoordinateFormatter { + return new CoordinateFormatter(); + } + + public function getFileUrlFinder(): FileUrlFinder { + return new MediaWikiFileUrlFinder(); + } + + public function getMappingServices(): MappingServices { + return new MappingServices( + $this->settings['egMapsAvailableServices'], + $this->settings['egMapsDefaultService'], + new GoogleMapsService(), + new LeafletService() + ); + } + + public function getDisplayMapFunction(): DisplayMapFunction { + return new DisplayMapFunction( + $this->getMappingServices() + ); + } + +} diff --git a/www/wiki/extensions/Maps/src/MapsFunctions.php b/www/wiki/extensions/Maps/src/MapsFunctions.php new file mode 100644 index 00000000..47cd1358 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MapsFunctions.php @@ -0,0 +1,218 @@ +<?php + +namespace Maps; + +use Xml; + +/** + * A class that holds static helper functions for generic mapping-related functions. + * + * @deprecated + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +final class MapsFunctions { + + /** + * Encode a variable of unknown type to JavaScript. + * Arrays are converted to JS arrays, objects are converted to JS associative + * arrays (objects). So cast your PHP associative arrays to objects before + * passing them to here. + * + * This is a copy of + * + * @see Xml::encodeJsVar + * which fixes incorrect behaviour with floats. + * + * @since 0.7.1 + * + * @param mixed $value + * + * @return string + */ + public static function encodeJsVar( $value ) { + if ( is_bool( $value ) ) { + $s = $value ? 'true' : 'false'; + } elseif ( is_null( $value ) ) { + $s = 'null'; + } elseif ( is_int( $value ) || is_float( $value ) ) { + $s = $value; + } elseif ( is_array( $value ) && // Make sure it's not associative. + array_keys( $value ) === range( 0, count( $value ) - 1 ) || + count( $value ) == 0 + ) { + $s = '['; + foreach ( $value as $elt ) { + if ( $s != '[' ) { + $s .= ', '; + } + $s .= self::encodeJsVar( $elt ); + } + $s .= ']'; + } elseif ( is_object( $value ) || is_array( $value ) ) { + // Objects and associative arrays + $s = '{'; + foreach ( (array)$value as $name => $elt ) { + if ( $s != '{' ) { + $s .= ', '; + } + $s .= '"' . Xml::encodeJsVar( $name ) . '": ' . + self::encodeJsVar( $elt ); + } + $s .= '}'; + } else { + $s = '"' . Xml::encodeJsVar( $value ) . '"'; + } + return $s; + } + + /** + * This function returns the definitions for the parameters used by every map feature. + * + * @return array + */ + public static function getCommonParameters() { + $params = []; + + $params['mappingservice'] = [ + 'type' => 'string', + 'aliases' => 'service', + 'default' => $GLOBALS['egMapsDefaultService'], + 'values' => MapsFactory::globalInstance()->getMappingServices()->getAllNames(), + ]; + + $params['width'] = [ + 'type' => 'dimension', + 'allowauto' => true, + 'units' => [ 'px', 'ex', 'em', '%', '' ], + 'default' => $GLOBALS['egMapsMapWidth'], + ]; + + $params['height'] = [ + 'type' => 'dimension', + 'units' => [ 'px', 'ex', 'em', '' ], + 'default' => $GLOBALS['egMapsMapHeight'], + ]; + + $params['centre'] = [ + 'type' => 'string', + 'aliases' => [ 'center' ], + 'default' => false, + 'manipulatedefault' => false, + ]; + + // Give grep a chance to find the usages: + // maps-par-mappingservice, maps-par-geoservice, maps-par-width, + // maps-par-height, maps-par-centre + foreach ( $params as $name => &$data ) { + $data['name'] = $name; + $data['message'] = 'maps-par-' . $name; + } + + $params['title'] = [ + 'name' => 'title', + 'default' => $GLOBALS['egMapsDefaultTitle'], + ]; + + $params['label'] = [ + 'default' => $GLOBALS['egMapsDefaultLabel'], + 'aliases' => 'text', + ]; + + $params['icon'] = [ + 'default' => '', + ]; + + $params['visitedicon'] = [ + 'default' => '', + ]; + + $params['lines'] = [ + 'type' => 'mapsline', + 'default' => [], + 'delimiter' => ';', + 'islist' => true, + ]; + + $params['polygons'] = [ + 'type' => 'mapspolygon', + 'default' => [], + 'delimiter' => ';', + 'islist' => true, + ]; + + $params['circles'] = [ + 'type' => 'mapscircle', + 'default' => [], + 'delimiter' => ';', + 'islist' => true, + ]; + + $params['rectangles'] = [ + 'type' => 'mapsrectangle', + 'default' => [], + 'delimiter' => ';', + 'islist' => true, + ]; + + $params['wmsoverlay'] = [ + 'type' => 'wmsoverlay', + 'default' => false, + 'delimiter' => ' ', + ]; + + $params['maxzoom'] = [ + 'type' => 'integer', + 'default' => false, + 'manipulatedefault' => false, + 'dependencies' => 'minzoom', + ]; + + $params['minzoom'] = [ + 'type' => 'integer', + 'default' => false, + 'manipulatedefault' => false, + 'lowerbound' => 0, + ]; + + $params['copycoords'] = [ + 'type' => 'boolean', + 'default' => false, + ]; + + $params['static'] = [ + 'type' => 'boolean', + 'default' => false, + ]; + + // Give grep a chance to find the usages: + // maps-displaymap-par-title, maps-displaymap-par-label, maps-displaymap-par-icon, + // maps-displaymap-par-visitedicon, aps-displaymap-par-lines, maps-displaymap-par-polygons, + // maps-displaymap-par-circles, maps-displaymap-par-rectangles, maps-displaymap-par-wmsoverlay, + // maps-displaymap-par-maxzoom, maps-displaymap-par-minzoom, maps-displaymap-par-copycoords, + // maps-displaymap-par-static + foreach ( $params as $name => &$param ) { + if ( !array_key_exists( 'message', $param ) ) { + $param['message'] = 'maps-displaymap-par-' . $name; + } + } + + return $params; + } + + /** + * Resolves the url of images provided as wiki page; leaves others alone. + * + * @since 1.0 + * @deprecated + * + * @param string $file + * + * @return string + */ + public static function getFileUrl( $file ): string { + return MapsFactory::globalInstance()->getFileUrlFinder()->getUrlForFileName( $file ); + } + +} diff --git a/www/wiki/extensions/Maps/src/MapsSetup.php b/www/wiki/extensions/Maps/src/MapsSetup.php new file mode 100644 index 00000000..245d3915 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MapsSetup.php @@ -0,0 +1,206 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps; + +use DataValues\Geo\Parsers\LatLongParser; +use Maps\DataAccess\JsonFileParser; +use Maps\MediaWiki\Content\GeoJsonContent; +use Maps\MediaWiki\Content\GeoJsonContentHandler; +use Maps\MediaWiki\ParserHooks\CoordinatesFunction; +use Maps\MediaWiki\ParserHooks\DisplayMapFunction; +use Maps\MediaWiki\ParserHooks\DistanceFunction; +use Maps\MediaWiki\ParserHooks\FindDestinationFunction; +use Maps\MediaWiki\ParserHooks\GeocodeFunction; +use Maps\MediaWiki\ParserHooks\GeoDistanceFunction; +use Maps\MediaWiki\ParserHooks\MapsDocFunction; +use Maps\Presentation\WikitextParsers\CircleParser; +use Maps\Presentation\WikitextParsers\DistanceParser; +use Maps\Presentation\WikitextParsers\ImageOverlayParser; +use Maps\Presentation\WikitextParsers\LineParser; +use Maps\Presentation\WikitextParsers\LocationParser; +use Maps\Presentation\WikitextParsers\PolygonParser; +use Maps\Presentation\WikitextParsers\RectangleParser; +use Maps\Presentation\WikitextParsers\WmsOverlayParser; +use Parser; +use PPFrame; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class MapsSetup { + + private $mwGlobals; + + public function __construct( array &$mwGlobals ) { + $this->mwGlobals = $mwGlobals; + } + + public function setup() { + $this->defaultSettings(); + $this->registerAllTheThings(); + + if ( !$this->mwGlobals['egMapsDisableSmwIntegration'] && defined( 'SMW_VERSION' ) ) { + SemanticMaps::newFromMediaWikiGlobals( $this->mwGlobals )->initExtension(); + } + } + + private function registerAllTheThings() { + $this->registerParserHooks(); + $this->registerPermissions(); + $this->registerParameterTypes(); + $this->registerHooks(); + + $this->mwGlobals['wgContentHandlers'][GeoJsonContent::CONTENT_MODEL_ID] = GeoJsonContentHandler::class; + } + + private function defaultSettings() { + if ( $this->mwGlobals['egMapsGMaps3Language'] === '' ) { + $this->mwGlobals['egMapsGMaps3Language'] = $this->mwGlobals['wgLang']; + } + + if ( in_array( 'googlemaps3', $this->mwGlobals['egMapsAvailableServices'] ) ) { + $this->mwGlobals['wgSpecialPages']['MapEditor'] = 'Maps\MediaWiki\Specials\SpecialMapEditor'; + $this->mwGlobals['wgSpecialPageGroups']['MapEditor'] = 'maps'; + } + + if ( $this->mwGlobals['egMapsGMaps3ApiKey'] === '' && array_key_exists( + 'egGoogleJsApiKey', + $this->mwGlobals + ) ) { + $this->mwGlobals['egMapsGMaps3ApiKey'] = $this->mwGlobals['egGoogleJsApiKey']; + } + } + + private function registerParserHooks() { + if ( $this->mwGlobals['egMapsEnableCoordinateFunction'] ) { + $this->mwGlobals['wgHooks']['ParserFirstCallInit'][] = function ( Parser &$parser ) { + return ( new CoordinatesFunction() )->init( $parser ); + }; + } + + $this->mwGlobals['wgHooks']['ParserFirstCallInit'][] = function ( Parser &$parser ) { + foreach ( [ 'display_map', 'display_point', 'display_points', 'display_line' ] as $hookName ) { + $parser->setFunctionHook( + $hookName, + function ( Parser $parser, PPFrame $frame, array $arguments ) { + $mapHtml = MapsFactory::newDefault()->getDisplayMapFunction()->getMapHtmlForKeyValueStrings( + $parser, + array_map( + function ( $argument ) use ( $frame ) { + return $frame->expand( $argument ); + }, + $arguments + ) + ); + + return [ + $mapHtml, + 'noparse' => true, + 'isHTML' => true, + ]; + }, + Parser::SFH_OBJECT_ARGS + ); + + $parser->setHook( + $hookName, + function ( $text, array $arguments, Parser $parser ) { + if ( $text !== null ) { + $defaultParameters = DisplayMapFunction::getHookDefinition( "\n" )->getDefaultParameters(); + $defaultParam = array_shift( $defaultParameters ); + + // If there is a first default parameter, set the tag contents as its value. + if ( $defaultParam !== null ) { + $arguments[$defaultParam] = $text; + } + } + + return MapsFactory::newDefault()->getDisplayMapFunction()->getMapHtmlForParameterList( $parser, $arguments ); + } + ); + } + }; + + $this->mwGlobals['wgHooks']['ParserFirstCallInit'][] = function ( Parser &$parser ) { + return ( new DistanceFunction() )->init( $parser ); + }; + + $this->mwGlobals['wgHooks']['ParserFirstCallInit'][] = function ( Parser &$parser ) { + return ( new FindDestinationFunction() )->init( $parser ); + }; + + $this->mwGlobals['wgHooks']['ParserFirstCallInit'][] = function ( Parser &$parser ) { + return ( new GeocodeFunction() )->init( $parser ); + }; + + $this->mwGlobals['wgHooks']['ParserFirstCallInit'][] = function ( Parser &$parser ) { + return ( new GeoDistanceFunction() )->init( $parser ); + }; + + $this->mwGlobals['wgHooks']['ParserFirstCallInit'][] = function ( Parser &$parser ) { + return ( new MapsDocFunction() )->init( $parser ); + }; + } + + private function registerPermissions() { + $this->mwGlobals['wgAvailableRights'][] = 'geocode'; + + // Users that can geocode. By default the same as those that can edit. + foreach ( $this->mwGlobals['wgGroupPermissions'] as $group => $rights ) { + if ( array_key_exists( 'edit', $rights ) ) { + $this->mwGlobals['wgGroupPermissions'][$group]['geocode'] = $this->mwGlobals['wgGroupPermissions'][$group]['edit']; + } + } + } + + private function registerParameterTypes() { + $this->mwGlobals['wgParamDefinitions']['coordinate'] = [ + 'string-parser' => LatLongParser::class, + ]; + + $this->mwGlobals['wgParamDefinitions']['mapslocation'] = [ + 'string-parser' => LocationParser::class, + ]; + + $this->mwGlobals['wgParamDefinitions']['mapsline'] = [ + 'string-parser' => LineParser::class, + ]; + + $this->mwGlobals['wgParamDefinitions']['mapscircle'] = [ + 'string-parser' => CircleParser::class, + ]; + + $this->mwGlobals['wgParamDefinitions']['mapsrectangle'] = [ + 'string-parser' => RectangleParser::class, + ]; + + $this->mwGlobals['wgParamDefinitions']['mapspolygon'] = [ + 'string-parser' => PolygonParser::class, + ]; + + $this->mwGlobals['wgParamDefinitions']['distance'] = [ + 'string-parser' => DistanceParser::class, + ]; + + $this->mwGlobals['wgParamDefinitions']['wmsoverlay'] = [ + 'string-parser' => WmsOverlayParser::class, + ]; + + $this->mwGlobals['wgParamDefinitions']['mapsimageoverlay'] = [ + 'string-parser' => ImageOverlayParser::class, + ]; + + $this->mwGlobals['wgParamDefinitions']['jsonfile'] = [ + 'string-parser' => JsonFileParser::class, + ]; + } + + private function registerHooks() { + $this->mwGlobals['wgHooks']['AdminLinks'][] = 'Maps\MediaWiki\MapsHooks::addToAdminLinks'; + $this->mwGlobals['wgHooks']['MakeGlobalVariablesScript'][] = 'Maps\MediaWiki\MapsHooks::onMakeGlobalVariablesScript'; + } + +} diff --git a/www/wiki/extensions/Maps/src/MediaWiki/Content/GeoJsonContent.php b/www/wiki/extensions/Maps/src/MediaWiki/Content/GeoJsonContent.php new file mode 100644 index 00000000..72a89b04 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/Content/GeoJsonContent.php @@ -0,0 +1,49 @@ +<?php + +namespace Maps\MediaWiki\Content; + +use Html; +use ParserOptions; +use ParserOutput; +use Title; + +class GeoJsonContent extends \JsonContent { + + public const CONTENT_MODEL_ID = 'GeoJSON'; + + public function __construct( string $text, string $modelId = self::CONTENT_MODEL_ID ) { + parent::__construct( $text, $modelId ); + } + + protected function fillParserOutput( Title $title, $revId, ParserOptions $options, + $generateHtml, ParserOutput &$output ) { + + if ( $generateHtml && $this->isValid() ) { + $output->setText( $this->getMapHtml( $this->beautifyJSON() ) ); + $output->addModules( 'ext.maps.leaflet.editor' ); + } else { + $output->setText( '' ); + } + } + + private function getMapHtml( string $jsonString ): string { + return + Html::element( + 'div', + [ + 'id' => 'GeoJsonMap', + 'class' => 'GeoJsonMap', + ] + ) + . '<style>' + . '.GeoJsonMap {width: "100%"; height: 600px; display: "inline-block"}' + . '</style>' + . + Html::element( + 'script', + [], + 'var GeoJson =' . $jsonString . ';' + ); + } + +}
\ No newline at end of file diff --git a/www/wiki/extensions/Maps/src/MediaWiki/Content/GeoJsonContentHandler.php b/www/wiki/extensions/Maps/src/MediaWiki/Content/GeoJsonContentHandler.php new file mode 100644 index 00000000..b192a9c3 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/Content/GeoJsonContentHandler.php @@ -0,0 +1,15 @@ +<?php + +namespace Maps\MediaWiki\Content; + +class GeoJsonContentHandler extends \JsonContentHandler { + + public function __construct( $modelId = GeoJsonContent::CONTENT_MODEL_ID ) { + parent::__construct( $modelId ); + } + + protected function getContentClass() { + return GeoJsonContent::class; + } + +}
\ No newline at end of file diff --git a/www/wiki/extensions/Maps/src/MediaWiki/MapsHooks.php b/www/wiki/extensions/Maps/src/MediaWiki/MapsHooks.php new file mode 100644 index 00000000..e2a8ad95 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/MapsHooks.php @@ -0,0 +1,68 @@ +<?php + +namespace Maps\MediaWiki; + +use AlItem; +use ALTree; + +/** + * Static class for hooks handled by the Maps extension. + * + * @since 0.7 + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +final class MapsHooks { + + /** + * Adds a link to Admin Links page. + * + * @since 0.7 + * + * @param ALTree $admin_links_tree + * + * @return boolean + */ + public static function addToAdminLinks( ALTree &$admin_links_tree ) { + $displaying_data_section = $admin_links_tree->getSection( + wfMessage( 'smw_adminlinks_displayingdata' )->text() + ); + + // Escape if SMW hasn't added links. + if ( is_null( $displaying_data_section ) ) { + return true; + } + + $smw_docu_row = $displaying_data_section->getRow( 'smw' ); + + $maps_docu_label = wfMessage( 'adminlinks_documentation', 'Maps' )->text(); + $smw_docu_row->addItem( + AlItem::newFromExternalLink( 'https://www.semantic-mediawiki.org/wiki/Extension:Maps', $maps_docu_label ) + ); + + return true; + } + + /** + * Adds global JavaScript variables. + * + * @since 1.0 + * @see http://www.mediawiki.org/wiki/Manual:Hooks/MakeGlobalVariablesScript + * + * @param array &$vars Variables to be added into the output + * + * @return boolean true in all cases + */ + public static function onMakeGlobalVariablesScript( array &$vars ) { + $vars['egMapsScriptPath'] = $GLOBALS['wgScriptPath'] . '/extensions/Maps/'; // TODO: wgExtensionDirectory? + $vars['egMapsDebugJS'] = $GLOBALS['egMapsDebugJS']; + $vars['egMapsAvailableServices'] = $GLOBALS['egMapsAvailableServices']; + $vars['egMapsLeafletLayersApiKeys'] = $GLOBALS['egMapsLeafletLayersApiKeys']; + + $vars += $GLOBALS['egMapsGlobalJSVars']; + + return true; + } + +} diff --git a/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/CoordinatesFunction.php b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/CoordinatesFunction.php new file mode 100644 index 00000000..94051571 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/CoordinatesFunction.php @@ -0,0 +1,102 @@ +<?php + +namespace Maps\MediaWiki\ParserHooks; + +use Maps\MapsFactory; +use ParserHook; + +/** + * Class for the 'coordinates' parser hooks, + * which can transform the notation of a set of coordinates. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class CoordinatesFunction extends ParserHook { + + /** + * Renders and returns the output. + * + * @see ParserHook::render + * + * @param array $parameters + * + * @return string + */ + public function render( array $parameters ) { + return MapsFactory::globalInstance()->getCoordinateFormatter()->format( + $parameters['location'], + $parameters['format'], + $parameters['directional'] + ); + } + + /** + * @see ParserHook::getMessage() + */ + public function getMessage() { + return 'maps-coordinates-description'; + } + + /** + * Gets the name of the parser hook. + * + * @see ParserHook::getName + * + * @return string + */ + protected function getName() { + return 'coordinates'; + } + + /** + * Returns an array containing the parameter info. + * + * @see ParserHook::getParameterInfo + * + * @return array + */ + protected function getParameterInfo( $type ) { + global $egMapsAvailableCoordNotations; + global $egMapsCoordinateNotation; + global $egMapsCoordinateDirectional; + + $params = []; + + $params['location'] = [ + 'type' => 'coordinate', + ]; + + $params['format'] = [ + 'default' => $egMapsCoordinateNotation, + 'values' => $egMapsAvailableCoordNotations, + 'aliases' => 'notation', + 'tolower' => true, + ]; + + $params['directional'] = [ + 'type' => 'boolean', + 'default' => $egMapsCoordinateDirectional, + ]; + + // Give grep a chance to find the usages: + // maps-coordinates-par-location, maps-coordinates-par-format, maps-coordinates-par-directional + foreach ( $params as $name => &$param ) { + $param['message'] = 'maps-coordinates-par-' . $name; + } + + return $params; + } + + /** + * Returns the list of default parameters. + * + * @see ParserHook::getDefaultParameters + * + * @return array + */ + protected function getDefaultParameters( $type ) { + return [ 'location', 'format', 'directional' ]; + } + +}
\ No newline at end of file diff --git a/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DisplayMapFunction.php b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DisplayMapFunction.php new file mode 100644 index 00000000..bad0d842 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DisplayMapFunction.php @@ -0,0 +1,171 @@ +<?php + +namespace Maps\MediaWiki\ParserHooks; + +use Maps; +use Maps\MapsFunctions; +use Maps\MappingServices; +use Maps\Presentation\ParameterExtractor; +use MWException; +use ParamProcessor; +use ParamProcessor\ProcessedParam; +use Parser; + +/** + * Class for the 'display_map' parser hooks. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class DisplayMapFunction { + + private $services; + + private $renderer; + + public function __construct( MappingServices $services ) { + $this->services = $services; + + $this->renderer = new DisplayMapRenderer(); + } + + /** + * @param Parser $parser + * @param string[] $parameters Values of the array can be named parameters ("key=value") or unnamed. + * They are not normalized, so can be "key = value " + * + * @return string + * @throws MWException + */ + public function getMapHtmlForKeyValueStrings( Parser $parser, array $parameters ): string { + $processor = new \ParamProcessor\Processor( new \ParamProcessor\Options() ); + + $service = $this->services->getServiceOrDefault( + $this->extractServiceName( + Maps\Presentation\ParameterExtractor::extractFromKeyValueStrings( $parameters ) + ) + ); + + $this->renderer->service = $service; + + $processor->setFunctionParams( + $parameters, + array_merge( + self::getHookDefinition( ';' )->getParameters(), + $service->getParameterInfo() + ), + self::getHookDefinition( ';' )->getDefaultParameters() + ); + + return $this->getMapHtmlFromProcessor( $parser, $processor ); + } + + /** + * @param Parser $parser + * @param string[] $parameters Key value list of parameters. Unnamed parameters have numeric keys. + * Both keys and values have not been normalized. + * + * @return string + * @throws MWException + */ + public function getMapHtmlForParameterList( Parser $parser, array $parameters ) { + $processor = new \ParamProcessor\Processor( new \ParamProcessor\Options() ); + + $service = $this->services->getServiceOrDefault( $this->extractServiceName( $parameters ) ); + + $this->renderer->service = $service; + + $processor->setParameters( + $parameters, + array_merge( + self::getHookDefinition( "\n" )->getParameters(), + $service->getParameterInfo() + ) + ); + + return $this->getMapHtmlFromProcessor( $parser, $processor ); + } + + private function getMapHtmlFromProcessor( Parser $parser, ParamProcessor\Processor $processor ) { + $params = $processor->processParameters()->getParameters(); + + $this->defaultMapZoom( $params ); + + $this->trackMap( $parser ); + + return $this->renderer->renderMap( + $this->processedParametersToKeyValueArray( $params ), + $parser + ); + } + + private function extractServiceName( array $parameters ): string { + $service = ( new ParameterExtractor() )->extract( + [ 'mappingservice', 'service' ], + $parameters + ); + + return $service ?? ''; + } + + private function processedParametersToKeyValueArray( array $params ): array { + $parameters = []; + + foreach ( $params as $parameter ) { + $parameters[$parameter->getName()] = $parameter->getValue(); + } + + return $parameters; + } + + public static function getHookDefinition( string $locationDelimiter ): \ParserHooks\HookDefinition { + return new \ParserHooks\HookDefinition( + [ 'display_map', 'display_point', 'display_points', 'display_line' ], + self::getParameterDefinitions( $locationDelimiter ), + [ 'coordinates' ] + ); + } + + private static function getParameterDefinitions( $locationDelimiter ): array { + $params = MapsFunctions::getCommonParameters(); + + $params['coordinates'] = [ + 'type' => 'string', + 'aliases' => [ 'coords', 'location', 'address', 'addresses', 'locations', 'points' ], + 'default' => [], + 'islist' => true, + 'delimiter' => $locationDelimiter, + 'message' => 'maps-displaymap-par-coordinates', + ]; + + return $params; + } + + /** + * @param ProcessedParam[] $parameters + */ + private function defaultMapZoom( array &$parameters ) { + if ( array_key_exists( 'zoom', $parameters ) && $parameters['zoom']->wasSetToDefault() && count( + $parameters['coordinates']->getValue() + ) > 1 ) { + $parameters['zoom'] = $this->getParameterWithValue( $parameters['zoom'], false ); + } + } + + private function getParameterWithValue( ProcessedParam $param, $value ) { + return new ProcessedParam( + $param->getName(), + $value, + $param->wasSetToDefault(), + $param->getOriginalName(), + $param->getOriginalValue() + ); + } + + private function trackMap( Parser $parser ) { + if ( $GLOBALS['egMapsEnableCategory'] ) { + $parser->addTrackingCategory( 'maps-tracking-category' ); + } + } + +} diff --git a/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DisplayMapRenderer.php b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DisplayMapRenderer.php new file mode 100644 index 00000000..8c757acd --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DisplayMapRenderer.php @@ -0,0 +1,182 @@ +<?php + +namespace Maps\MediaWiki\ParserHooks; + +use FormatJson; +use Html; +use Maps\DataAccess\MediaWikiFileUrlFinder; +use Maps\Elements\Location; +use Maps\MappingService; +use Maps\Presentation\ElementJsonSerializer; +use Maps\Presentation\MapHtmlBuilder; +use Maps\Presentation\WikitextParser; +use Maps\Presentation\WikitextParsers\LocationParser; +use Parser; + +/** + * Class handling the #display_map rendering. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @author Kim Eik + */ +class DisplayMapRenderer { + + public $service; + + /** + * @var LocationParser + */ + private $locationParser; + + /** + * @var MediaWikiFileUrlFinder + */ + private $fileUrlFinder; + + /** + * @var WikitextParser + */ + private $wikitextParser; + /** + * @var ElementJsonSerializer + */ + private $elementSerializer; + + public function __construct( MappingService $service = null ) { + $this->service = $service; + } + + /** + * Handles the request from the parser hook by doing the work that's common for all + * mapping services, calling the specific methods and finally returning the resulting output. + * + * @param array $params + * @param Parser $parser + * + * @return string + */ + public final function renderMap( array $params, Parser $parser ) { + $factory = \Maps\MapsFactory::newDefault(); + + $this->locationParser = $factory->newLocationParser(); + $this->fileUrlFinder = $factory->getFileUrlFinder(); + + $this->wikitextParser = new WikitextParser( clone $parser ); + $this->elementSerializer = new ElementJsonSerializer( $this->wikitextParser ); + + $this->handleMarkerData( $params ); + + $output = ( new MapHtmlBuilder() )->getMapHTML( + $params, + $this->service->newMapId(), + $this->service->getName() + ); + + $dependencies = $this->service->getDependencyHtml( $params ); + + // Only add a head item when there are dependencies. + if ( $dependencies ) { + $parser->getOutput()->addHeadItem( $dependencies ); + } + + $parser->getOutput()->addModules( $this->service->getResourceModules() ); + + return $output; + } + + /** + * Converts the data in the coordinates parameter to JSON-ready objects. + * These get stored in the locations parameter, and the coordinates on gets deleted. + */ + private function handleMarkerData( array &$params ) { + $params['centre'] = $this->getCenter( $params['centre'] ); + + if ( is_object( $params['wmsoverlay'] ) ) { + $params['wmsoverlay'] = $params['wmsoverlay']->getJSONObject(); + } + + $params['locations'] = $this->getLocationJson( $params ); + + unset( $params['coordinates'] ); + + $this->handleShapeData( $params ); + } + + private function getCenter( $coordinatesOrAddress ) { + if ( $coordinatesOrAddress === false ) { + return false; + } + + try { + // FIXME: a Location makes no sense here, since the non-coordinate data is not used + $location = $this->locationParser->parse( $coordinatesOrAddress ); + } + catch ( \Exception $ex ) { + // TODO: somehow report this to the user + return false; + } + + return $location->getJSONObject(); + } + + private function getLocationJson( array $params ) { + $iconUrl = $this->fileUrlFinder->getUrlForFileName( $params['icon'] ); + $visitedIconUrl = $this->fileUrlFinder->getUrlForFileName( $params['visitedicon'] ); + + $locationJsonObjects = []; + + foreach ( $params['coordinates'] as $coordinatesOrAddress ) { + try { + $location = $this->locationParser->parse( $coordinatesOrAddress ); + } + catch ( \Exception $ex ) { + // TODO: somehow report this to the user + continue; + } + + $locationJsonObjects[] = $this->getLocationJsonObject( + $location, + $params, + $iconUrl, + $visitedIconUrl + ); + } + + return $locationJsonObjects; + } + + private function getLocationJsonObject( Location $location, array $params, $iconUrl, $visitedIconUrl ) { + $jsonObj = $location->getJSONObject( $params['title'], $params['label'], $iconUrl, '', '', $visitedIconUrl ); + + $this->elementSerializer->titleAndText( $jsonObj ); + + if ( isset( $jsonObj['inlineLabel'] ) ) { + $jsonObj['inlineLabel'] = strip_tags( + $this->wikitextParser->wikitextToHtml( $jsonObj['inlineLabel'] ), + '<a><img>' + ); + } + + return $jsonObj; + } + + private function handleShapeData( array &$params ) { + $textContainers = [ + &$params['lines'], + &$params['polygons'], + &$params['circles'], + &$params['rectangles'], + &$params['imageoverlays'], // FIXME: this is Google Maps specific!! + ]; + + foreach ( $textContainers as &$textContainer ) { + if ( is_array( $textContainer ) ) { + foreach ( $textContainer as &$obj ) { + $obj = $this->elementSerializer->elementToJson( $obj ); + } + } + } + } + +} diff --git a/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DistanceFunction.php b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DistanceFunction.php new file mode 100644 index 00000000..84b11c2b --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DistanceFunction.php @@ -0,0 +1,100 @@ +<?php + +namespace Maps\MediaWiki\ParserHooks; + +use Maps\Presentation\MapsDistanceParser; +use ParserHook; + +/** + * Class for the 'distance' parser hooks, + * which can transform the notation of a distance. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class DistanceFunction extends ParserHook { + + /** + * Renders and returns the output. + * + * @see ParserHook::render + * + * @param array $parameters + * + * @return string + */ + public function render( array $parameters ) { + return MapsDistanceParser::formatDistance( + $parameters['distance'], + $parameters['unit'], + $parameters['decimals'] + ); + } + + /** + * @see ParserHook::getMessage() + */ + public function getMessage() { + return 'maps-distance-description'; + } + + /** + * Gets the name of the parser hook. + * + * @see ParserHook::getName + * + * @return string + */ + protected function getName() { + return 'distance'; + } + + /** + * Returns an array containing the parameter info. + * + * @see ParserHook::getParameterInfo + * + * @return array + */ + protected function getParameterInfo( $type ) { + global $egMapsDistanceUnit, $egMapsDistanceDecimals; + + $params = []; + + $params['distance'] = [ + 'type' => 'distance', + ]; + + $params['unit'] = [ + 'default' => $egMapsDistanceUnit, + 'values' => MapsDistanceParser::getUnits(), + ]; + + $params['decimals'] = [ + 'type' => 'integer', + 'default' => $egMapsDistanceDecimals, + ]; + + // Give grep a chance to find the usages: + // maps-distance-par-distance, maps-distance-par-unit, maps-distance-par-decimals + foreach ( $params as $name => &$param ) { + $param['message'] = 'maps-distance-par-' . $name; + } + + return $params; + } + + /** + * Returns the list of default parameters. + * + * @see ParserHook::getDefaultParameters + * + * @param $type + * + * @return array + */ + protected function getDefaultParameters( $type ) { + return [ 'distance', 'unit', 'decimals' ]; + } + +}
\ No newline at end of file diff --git a/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/FindDestinationFunction.php b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/FindDestinationFunction.php new file mode 100644 index 00000000..e7cb319c --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/FindDestinationFunction.php @@ -0,0 +1,120 @@ +<?php + +namespace Maps\MediaWiki\ParserHooks; + +use DataValues\Geo\Values\LatLongValue; +use Maps\MapsFactory; +use Maps\GeoFunctions; +use ParserHook; + +/** + * Class for the 'finddestination' parser hooks, which can find a + * destination given a starting point, an initial bearing and a distance. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class FindDestinationFunction extends ParserHook { + + /** + * Renders and returns the output. + * + * @see ParserHook::render + * + * @param array $parameters + * + * @return string + */ + public function render( array $parameters ) { + $destination = GeoFunctions::findDestination( + $parameters['location']->getCoordinates(), + $parameters['bearing'], + $parameters['distance'] + ); + + return MapsFactory::globalInstance()->getCoordinateFormatter()->format( + new LatLongValue( $destination['lat'], $destination['lon'] ), + $parameters['format'], + $parameters['directional'] + ); + } + + /** + * @see ParserHook::getMessage() + */ + public function getMessage() { + return 'maps-finddestination-description'; + } + + /** + * Gets the name of the parser hook. + * + * @see ParserHook::getName + * + * @return string + */ + protected function getName() { + return 'finddestination'; + } + + /** + * Returns an array containing the parameter info. + * + * @see ParserHook::getParameterInfo + * + * @return array + */ + protected function getParameterInfo( $type ) { + global $egMapsAvailableCoordNotations; + global $egMapsCoordinateNotation, $egMapsCoordinateDirectional; + + $params = []; + + $params['location'] = [ + 'type' => 'mapslocation', + ]; + + $params['format'] = [ + 'default' => $egMapsCoordinateNotation, + 'values' => $egMapsAvailableCoordNotations, + 'aliases' => 'notation', + 'tolower' => true, + ]; + + $params['directional'] = [ + 'type' => 'boolean', + 'default' => $egMapsCoordinateDirectional, + ]; + + $params['bearing'] = [ + 'type' => 'float', + ]; + + $params['distance'] = [ + 'type' => 'distance', + ]; + + // Give grep a chance to find the usages: + // maps-finddestination-par-location, maps-finddestination-par-format, + // maps-finddestination-par-directional, maps-finddestination-par-bearing, + // maps-finddestination-par-distance, maps-finddestination-par-mappingservice, + // maps-finddestination-par-geoservice, maps-finddestination-par-allowcoordinates + foreach ( $params as $name => &$param ) { + $param['message'] = 'maps-finddestination-par-' . $name; + } + + return $params; + } + + /** + * Returns the list of default parameters. + * + * @see ParserHook::getDefaultParameters + * + * @return array + */ + protected function getDefaultParameters( $type ) { + return [ 'location', 'bearing', 'distance' ]; + } + +}
\ No newline at end of file diff --git a/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/GeoDistanceFunction.php b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/GeoDistanceFunction.php new file mode 100644 index 00000000..f7afc393 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/GeoDistanceFunction.php @@ -0,0 +1,117 @@ +<?php + +namespace Maps\MediaWiki\ParserHooks; + +use Maps\GeoFunctions; +use Maps\Presentation\MapsDistanceParser; +use MWException; +use ParserHook; + +/** + * Class for the 'geodistance' parser hooks, which can + * calculate the geographical distance between two points. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class GeoDistanceFunction extends ParserHook { + + /** + * Renders and returns the output. + * + * @see ParserHook::render + * + * @param array $parameters + * + * @return string + * @throws MWException + */ + public function render( array $parameters ) { + /** + * @var \DataValues\Geo\Values\LatLongValue $coordinates1 + * @var \DataValues\Geo\Values\LatLongValue $coordinates2 + */ + $coordinates1 = $parameters['location1']->getCoordinates(); + $coordinates2 = $parameters['location2']->getCoordinates(); + + $distance = GeoFunctions::calculateDistance( $coordinates1, $coordinates2 ); + $output = MapsDistanceParser::formatDistance( $distance, $parameters['unit'], $parameters['decimals'] ); + + return $output; + } + + /** + * @see ParserHook::getMessage + */ + public function getMessage() { + return 'maps-geodistance-description'; + } + + /** + * Gets the name of the parser hook. + * + * @see ParserHook::getName + * + * @return string + */ + protected function getName() { + return 'geodistance'; + } + + /** + * Returns an array containing the parameter info. + * + * @see ParserHook::getParameterInfo + * + * @return array + */ + protected function getParameterInfo( $type ) { + global $egMapsDistanceUnit, $egMapsDistanceDecimals; + + $params = []; + + $params['unit'] = [ + 'default' => $egMapsDistanceUnit, + 'values' => MapsDistanceParser::getUnits(), + ]; + + $params['decimals'] = [ + 'type' => 'integer', + 'default' => $egMapsDistanceDecimals, + ]; + + $params['location1'] = [ + 'type' => 'mapslocation', + 'aliases' => 'from', + ]; + + $params['location2'] = [ + 'type' => 'mapslocation', + 'aliases' => 'to', + ]; + + // Give grep a chance to find the usages: + // maps-geodistance-par-mappingservice, maps-geodistance-par-geoservice, + // maps-geodistance-par-unit, maps-geodistance-par-decimals, + // maps-geodistance-par-location1, maps-geodistance-par-location2 + foreach ( $params as $name => &$param ) { + $param['message'] = 'maps-geodistance-par-' . $name; + } + + return $params; + } + + /** + * Returns the list of default parameters. + * + * @see ParserHook::getDefaultParameters + * + * @param $type + * + * @return array + */ + protected function getDefaultParameters( $type ) { + return [ 'location1', 'location2', 'unit', 'decimals' ]; + } + +}
\ No newline at end of file diff --git a/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/GeocodeFunction.php b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/GeocodeFunction.php new file mode 100644 index 00000000..5f70ef24 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/GeocodeFunction.php @@ -0,0 +1,113 @@ +<?php + +namespace Maps\MediaWiki\ParserHooks; + +use Jeroen\SimpleGeocoder\Geocoder; +use Maps\MapsFactory; +use ParserHook; + +/** + * Class for the 'geocode' parser hooks, which can turn + * human readable locations into sets of coordinates. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class GeocodeFunction extends ParserHook { + + private $geocoder; + + public function __construct( Geocoder $geocoder = null ) { + $this->geocoder = $geocoder ?? \Maps\MapsFactory::newDefault()->getGeocoder(); + parent::__construct(); + } + + /** + * Renders and returns the output. + * + * @see ParserHook::render + * + * @param array $parameters + * + * @return string + */ + public function render( array $parameters ) { + $coordinates = $this->geocoder->geocode( $parameters['location'] ); + + if ( $coordinates === null ) { + return 'Geocoding failed'; // TODO: i18n + } + + return MapsFactory::globalInstance()->getCoordinateFormatter()->format( + $coordinates, + $parameters['format'], + $parameters['directional'] + ); + } + + /** + * @see ParserHook::getMessage() + */ + public function getMessage() { + return 'maps-geocode-description'; + } + + /** + * Gets the name of the parser hook. + * + * @see ParserHook::getName + * + * @return string + */ + protected function getName() { + return 'geocode'; + } + + /** + * Returns an array containing the parameter info. + * + * @see ParserHook::getParameterInfo + * + * @return array + */ + protected function getParameterInfo( $type ) { + global $egMapsAvailableCoordNotations; + global $egMapsCoordinateNotation; + global $egMapsCoordinateDirectional; + + $params = []; + + $params['location'] = [ + 'type' => 'string', + 'message' => 'maps-geocode-par-location', + ]; + + $params['format'] = [ + 'default' => $egMapsCoordinateNotation, + 'values' => $egMapsAvailableCoordNotations, + 'aliases' => 'notation', + 'tolower' => true, + 'message' => 'maps-geocode-par-format', + ]; + + $params['directional'] = [ + 'type' => 'boolean', + 'default' => $egMapsCoordinateDirectional, + 'message' => 'maps-geocode-par-directional', + ]; + + return $params; + } + + /** + * Returns the list of default parameters. + * + * @see ParserHook::getDefaultParameters + * + * @return array + */ + protected function getDefaultParameters( $type ) { + return [ 'location' ]; + } + +} diff --git a/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/MapsDocFunction.php b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/MapsDocFunction.php new file mode 100644 index 00000000..6a365378 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/MapsDocFunction.php @@ -0,0 +1,200 @@ +<?php + +namespace Maps\MediaWiki\ParserHooks; + +use Maps\MappingServices; +use Maps\MapsFactory; +use ParamProcessor\ParamDefinition; +use ParserHook; + +/** + * Class for the 'mapsdoc' parser hooks, + * which displays documentation for a specified mapping service. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class MapsDocFunction extends ParserHook { + + /** + * Field to store the value of the language parameter. + * + * @var string + */ + protected $language; + + /** + * Renders and returns the output. + * + * @see ParserHook::render + * + * @param array $parameters + * + * @return string + */ + public function render( array $parameters ) { + $this->language = $parameters['language']; + + $params = $this->getServiceParameters( $parameters['service'] ); + + return $this->getParameterTable( $params ); + } + + private function getServiceParameters( $service ) { + return array_merge( + [ + 'zoom' => [ + 'type' => 'integer', + 'message' => 'maps-par-zoom', + ] + ], + MapsFactory::globalInstance()->getMappingServices()->getService( $service )->getParameterInfo() + ); + } + + /** + * Returns the wikitext for a table listing the provided parameters. + * + * @param array $parameters + * + * @return string + */ + private function getParameterTable( array $parameters ) { + $tableRows = []; + + $parameters = ParamDefinition::getCleanDefinitions( $parameters ); + + foreach ( $parameters as $parameter ) { + $tableRows[] = $this->getDescriptionRow( $parameter ); + } + + $table = ''; + + if ( count( $tableRows ) > 0 ) { + $tableRows = array_merge( + [ + '!' . $this->msg( 'validator-describe-header-parameter' ) . "\n" . + //'!' . $this->msg( 'validator-describe-header-aliases' ) ."\n" . + '!' . $this->msg( 'validator-describe-header-type' ) . "\n" . + '!' . $this->msg( 'validator-describe-header-default' ) . "\n" . + '!' . $this->msg( 'validator-describe-header-description' ) + ], + $tableRows + ); + + $table = implode( "\n|-\n", $tableRows ); + + $table = + '{| class="wikitable sortable"' . "\n" . + $table . + "\n|}"; + } + + return $table; + } + + /** + * Returns the wikitext for a table row describing a single parameter. + * + * @param ParamDefinition $parameter + * + * @return string + */ + private function getDescriptionRow( ParamDefinition $parameter ) { + $description = $this->msg( $parameter->getMessage() ); + + $type = $this->msg( $parameter->getTypeMessage() ); + + $default = $parameter->isRequired() ? "''" . $this->msg( + 'validator-describe-required' + ) . "''" : $parameter->getDefault(); + if ( is_array( $default ) ) { + $default = implode( ', ', $default ); + } elseif ( is_bool( $default ) ) { + $default = $default ? 'yes' : 'no'; + } + + if ( $default === '' ) { + $default = "''" . $this->msg( 'validator-describe-empty' ) . "''"; + } + + return <<<EOT +| {$parameter->getName()} +| {$type} +| {$default} +| {$description} +EOT; + } + + /** + * Message function that takes into account the language parameter. + * + * @param string $key + * @param ... $args + * + * @return string + */ + private function msg() { + $args = func_get_args(); + $key = array_shift( $args ); + return wfMessage( $key, $args )->inLanguage( $this->language )->text(); + } + + /** + * @see ParserHook::getDescription() + */ + public function getMessage() { + return 'maps-mapsdoc-description'; + } + + /** + * Gets the name of the parser hook. + * + * @see ParserHook::getName + * + * @return string + */ + protected function getName() { + return 'mapsdoc'; + } + + /** + * Returns an array containing the parameter info. + * + * @see ParserHook::getParameterInfo + * + * @return array + */ + protected function getParameterInfo( $type ) { + $params = []; + + $params['service'] = [ + 'values' => $GLOBALS['egMapsAvailableServices'], + 'tolower' => true, + ]; + + $params['language'] = [ + 'default' => $GLOBALS['wgLanguageCode'], + ]; + + // Give grep a chance to find the usages: + // maps-geocode-par-service, maps-geocode-par-language + foreach ( $params as $name => &$param ) { + $param['message'] = 'maps-geocode-par-' . $name; + } + + return $params; + } + + /** + * Returns the list of default parameters. + * + * @see ParserHook::getDefaultParameters + * + * @return array + */ + protected function getDefaultParameters( $type ) { + return [ 'service', 'language' ]; + } + +} diff --git a/www/wiki/extensions/Maps/src/MediaWiki/SemanticMapsHooks.php b/www/wiki/extensions/Maps/src/MediaWiki/SemanticMapsHooks.php new file mode 100644 index 00000000..4c929b42 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/SemanticMapsHooks.php @@ -0,0 +1,115 @@ +<?php + +namespace Maps\MediaWiki; + +use AlItem; +use ALTree; +use Maps\SemanticMW\DataValues\CoordinateValue; +use Maps\SemanticMW\DataValues\GeoPolygonValue; +use SMW\DataTypeRegistry; +use SMWDataItem; +use SMWPrintRequest; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +final class SemanticMapsHooks { + + /** + * Adds a link to Admin Links page. + * + * @since 0.7 + * + * @param ALTree $admin_links_tree + * + * @return boolean + */ + public static function addToAdminLinks( ALTree &$admin_links_tree ) { + $displaying_data_section = $admin_links_tree->getSection( + wfMessage( 'smw_adminlinks_displayingdata' )->text() + ); + + // Escape if SMW hasn't added links. + if ( is_null( $displaying_data_section ) ) { + return true; + } + + $smw_docu_row = $displaying_data_section->getRow( 'smw' ); + + $sm_docu_label = wfMessage( 'adminlinks_documentation', 'Semantic Maps' )->text(); + $smw_docu_row->addItem( + AlItem::newFromExternalLink( 'https://www.semantic-mediawiki.org/wiki/Semantic_Maps', $sm_docu_label ) + ); + + return true; + } + + /** + * Adds support for the geographical coordinates and shapes data type to Semantic MediaWiki. + * + * @since 2.0 + * + * @return boolean + */ + public static function initGeoDataTypes() { + DataTypeRegistry::getInstance()->registerDatatype( + '_geo', + CoordinateValue::class, + SMWDataItem::TYPE_GEO + ); + + return true; + } + + /** + * Set the default format to 'map' when the requested properties are + * of type geographic coordinates. + * + * TODO: have a setting to turn this off and have it off by default for #show + * + * @since 1.0 + * + * @param $format Mixed: The format (string), or false when not set yet + * @param SMWPrintRequest[] $printRequests The print requests made + * + * @return boolean + */ + public static function addGeoCoordsDefaultFormat( &$format, array $printRequests ) { + // Only set the format when not set yet. This allows other extensions to override the Maps behavior. + if ( $format === false ) { + // Only apply when there is more then one print request. + // This way requests comming from #show are ignored. + if ( count( $printRequests ) > 1 ) { + $allValid = true; + $hasCoords = false; + + // Loop through the print requests to determine their types. + foreach ( $printRequests as $printRequest ) { + // Skip the first request, as it's the object. + if ( $printRequest->getMode() == SMWPrintRequest::PRINT_THIS ) { + continue; + } + + $typeId = $printRequest->getTypeID(); + + if ( $typeId == '_geo' ) { + $hasCoords = true; + } else { + $allValid = false; + break; + } + } + + // If they are all coordinates, set the result format to 'map'. + if ( $allValid && $hasCoords ) { + $format = 'map'; + } + } + + } + + return true; + } + +} diff --git a/www/wiki/extensions/Maps/src/MediaWiki/Specials/MapEditorHTML.php b/www/wiki/extensions/Maps/src/MediaWiki/Specials/MapEditorHTML.php new file mode 100644 index 00000000..60409041 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/Specials/MapEditorHTML.php @@ -0,0 +1,221 @@ +<?php + +namespace Maps\MediaWiki\Specials; + +use ContextSource; +use Html; + +/** + * Class to Handle HTML generation for Special:MapEditor + * + * @since 2.1 + * + * @licence GNU GPL v2+ + * @author Nischayn22 + */ +class MapEditorHtml extends ContextSource { + + /** + * Array holding the additional attributes for the canvas div. + * + * @var array() + * @since 2.1 + */ + protected $attribs; + + public function __construct( $attribs = [] ) { + $this->attribs = $attribs; + } + + /** + * Returns the HTML for the MapEditor. + * + * @return string + * @since 2.1 + */ + public function getEditorHTML() { + + $output = <<<EOT + {$this->getCanvasDiv()}<div style="display: none;"> + <div id="code-output-container" title="%1\$s"> + <textarea id="code-output" rows="15" readonly></textarea> + </div> + <div id="code-input-container" title="%2\$s" > + <p>%3\$s</p> + <textarea id="code-input" rows="15"></textarea> + </div> + <div id="marker-form" class="mapeditor-dialog" title="%4\$s"> + <div class="link-title-switcher"> + <input type="radio" name="switch" value="text" /> %5\$s + <input type="radio" name="switch" value="link" /> %6\$s + </div> + <form class="mapeditor-dialog-form"> + <fieldset> + <label for="m-title">%7\$s</label> + <input type="text" name="title" id="m-title" class="text ui-widget-content ui-corner-all"/> + <label for="m-text">%8\$s</label> + <input type="text" name="text" id="m-text" class="text ui-widget-content ui-corner-all"/> + <label for="m-link">%9\$s</label> + <input type="text" name="link" id="m-link" class="text ui-widget-content ui-corner-all"/> + <label for="m-icon">%10\$s</label> + <input type="text" name="icon" id="m-icon" class="text ui-widget-content ui-corner-all"/> + <label for="m-group">%11\$s</label> + <input type="text" name="group" id="m-group" class="text ui-widget-content ui-corner-all"/> + <label for="m-inlinelabel">%12\$s</label> + <input type="text" name="inlinelabel" id="m-inlinelabel" class="text ui-widget-content ui-corner-all"/> + <label for="m-visitedicon">%23\$s</label> + <input type="text" name="visitedicon" id="m-visitedicon" class="text ui-widget-content ui-corner-all"/> + </fieldset> + </form> + </div> + + <div id="strokable-form" class="mapeditor-dialog" title="%4\$s"> + <div class="link-title-switcher"> + <input type="radio" name="switch" value="text" /> %5\$s + <input type="radio" name="switch" value="link" /> %6\$s + </div> + <form class="mapeditor-dialog-form"> + <fieldset> + <label for="s-title">%7\$s</label> + <input type="text" name="title" id="s-title" class="text ui-widget-content ui-corner-all"/> + <label for="s-text">%8\$s</label> + <input type="text" name="text" id="s-text" value="" class="text ui-widget-content ui-corner-all"/> + <label for="s-link">%9\$s</label> + <input type="text" name="link" id="s-link" class="text ui-widget-content ui-corner-all"/> + <label for="s-strokecolor">%13\$s</label> + <input type="text" name="strokeColor" id="s-strokecolor" class="text ui-widget-content ui-corner-all"/> + <label for="s-strokeopacity">%14\$s</label> + <input type="hidden" name="strokeOpacity" id="s-strokeopacity" class="text ui-widget-content ui-corner-all"/> + <label for="s-strokeweight">%15\$s</label> + <input type="text" name="strokeWeight" id="s-strokeweight" class="text ui-widget-content ui-corner-all"/> + </fieldset> + </form> + </div> + + <div id="fillable-form" class="mapeditor-dialog" title="%4\$s"> + <div class="link-title-switcher"> + <input type="radio" name="switch" value="text" /> %5\$s + <input type="radio" name="switch" value="link" /> %6\$s + </div> + <form class="mapeditor-dialog-form"> + <fieldset> + <label for="f-title">%7\$s</label> + <input type="text" name="title" id="f-title" class="text ui-widget-content ui-corner-all"/> + <label for="f-text">%8\$s</label> + <input type="text" name="text" id="f-text" value="" class="text ui-widget-content ui-corner-all"/> + <label for="f-link">%9\$s</label> + <input type="text" name="link" id="f-link" class="text ui-widget-content ui-corner-all"/> + <label for="f-strokecolor">%13\$s</label> + <input type="text" name="strokeColor" id="f-strokecolor" class="text ui-widget-content ui-corner-all"/> + <label for="f-strokeopacity">%14\$s</label> + <input type="hidden" name="strokeOpacity" id="f-strokeopacity" class="text ui-widget-content ui-corner-all"/> + <label for="f-strokeweight">%15\$s</label> + <input type="text" name="strokeWeight" id="f-strokeweight" class="text ui-widget-content ui-corner-all"/> + <label for="f-fillcolor">%16\$s</label> + <input type="text" name="fillColor" id="f-fillcolor" class="text ui-widget-content ui-corner-all"/> + <label for="f-fillopacity">%17\$s</label> + <input type="hidden" name="fillOpacity" id="f-fillopacity" class="text ui-widget-content ui-corner-all"/> + </fieldset> + </form> + </div> + + <div id="polygon-form" class="mapeditor-dialog" title="%4\$s"> + <div class="link-title-switcher"> + <input type="radio" name="switch" value="text" /> %5\$s + <input type="radio" name="switch" value="link" /> %6\$s + </div> + <form class="mapeditor-dialog-form"> + <fieldset> + <label for="p-title">%7\$s</label> + <input type="text" name="title" id="p-title" class="text ui-widget-content ui-corner-all"/> + <label for="p-text">%8\$s</label> + <input type="text" name="text" id="p-text" value="" class="text ui-widget-content ui-corner-all"/> + <label for="p-link">%9\$s</label> + <input type="text" name="link" id="p-link" class="text ui-widget-content ui-corner-all"/> + <label for="p-strokecolor">%13\$s</label> + <input type="text" name="strokeColor" id="p-strokecolor" class="text ui-widget-content ui-corner-all"/> + <label for="p-strokeopacity">%14\$s</label> + <input type="hidden" name="strokeOpacity" id="p-strokeopacity" class="text ui-widget-content ui-corner-all"/> + <label for="p-strokeweight">%15\$s</label> + <input type="text" name="strokeWeight" id="p-strokeweight" class="text ui-widget-content ui-corner-all"/> + <label for="p-fillcolor">%16\$s</label> + <input type="text" name="fillColor" id="p-fillcolor" class="text ui-widget-content ui-corner-all"/> + <label for="p-fillopacity">%17\$s</label> + <input type="hidden" name="fillOpacity" id="p-fillopacity" class="text ui-widget-content ui-corner-all"/> + <label for="p-showonhover">%18\$s</label> + <input type="checkbox" name="showOnHover" id="p-showonhover" class="text ui-widget-content ui-corner-all"/> + </fieldset> + </form> + </div> + <div id="map-parameter-form" class="mapeditor-dialog" title="%19\$s"> + <form class="mapeditor-dialog-form"> + <div> + <select name="key"> + <option value="">%20\$s</option> + </select> + </div> + </form> + </div> + <div id="imageoverlay-form" title="%22\$s"> + <div class="link-title-switcher"> + <input type="radio" name="switch" value="text" /> %5\$s + <input type="radio" name="switch" value="link" /> %6\$s + </div> + <form class="mapeditor-dialog-form"> + <fieldset> + <label for="i-title">%7\$s</label> + <input type="text" name="title" id="i-title" class="text ui-widget-content ui-corner-all"/> + <label for="i-text">%8\$s</label> + <input type="text" name="text" id="i-text" class="text ui-widget-content ui-corner-all"/> + <label for="i-link">%9\$s</label> + <input type="text" name="link" id="i-link" class="text ui-widget-content ui-corner-all"/> + <label for="i-image">%21\$s</label> + <input type="text" name="image" id="i-image" class="text ui-widget-content ui-corner-all"/> + </fieldset> + </form> + </div> +</div> +EOT; + + $html = sprintf( + $output, + $this->msg( 'mapeditor-code-title' ), + $this->msg( 'mapeditor-import-title' ), + $this->msg( 'mapeditor-import-note' ), + $this->msg( 'mapeditor-form-title' ), + $this->msg( 'mapeditor-link-title-switcher-popup-text' ), + $this->msg( 'mapeditor-link-title-switcher-link-text' ), + $this->msg( 'mapeditor-form-field-title' ), + $this->msg( 'mapeditor-form-field-text' ), + $this->msg( 'mapeditor-form-field-link' ), + $this->msg( 'mapeditor-form-field-icon' ), + $this->msg( 'mapeditor-form-field-group' ), + $this->msg( 'mapeditor-form-field-inlinelabel' ), + $this->msg( 'mapeditor-form-field-strokecolor' ), + $this->msg( 'mapeditor-form-field-strokeopacity' ), + $this->msg( 'mapeditor-form-field-strokeweight' ), + $this->msg( 'mapeditor-form-field-fillcolor' ), + $this->msg( 'mapeditor-form-field-fillopcaity' ), + $this->msg( 'mapeditor-form-field-showonhover' ), + $this->msg( 'mapeditor-mapparam-title' ), + $this->msg( 'mapeditor-mapparam-defoption' ), + $this->msg( 'mapeditor-form-field-image' ), + $this->msg( 'mapeditor-imageoverlay-title' ), + $this->msg( 'mapeditor-form-field-visitedicon' ) + ); + + return $html; + } + + /** + * @return string + * @since 2.1 + */ + public function getCanvasDiv() { + return Html::element( + 'div', + $this->attribs + ); + } + +}
\ No newline at end of file diff --git a/www/wiki/extensions/Maps/src/MediaWiki/Specials/SpecialMapEditor.php b/www/wiki/extensions/Maps/src/MediaWiki/Specials/SpecialMapEditor.php new file mode 100644 index 00000000..76b5dad5 --- /dev/null +++ b/www/wiki/extensions/Maps/src/MediaWiki/Specials/SpecialMapEditor.php @@ -0,0 +1,69 @@ +<?php + +namespace Maps\MediaWiki\Specials; + +use Maps\GoogleMapsService; +use Maps\MediaWiki\Specials\MapEditorHtml; +use SpecialPage; + +/** + * Special page with map editor interface using Google Maps. + * + * @since 2.0 + * + * @licence GNU GPL v2+ + * @author Kim Eik + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class SpecialMapEditor extends SpecialPage { + + /** + * @see SpecialPage::__construct + * + * @since 2.0 + */ + public function __construct() { + parent::__construct( 'MapEditor' ); + } + + /** + * @see SpecialPage::execute + * + * @since 2.0 + * + * @param null|string $subPage + */ + public function execute( $subPage ) { + $this->setHeaders(); + + $outputPage = $this->getOutput(); + + $outputPage->addHtml( + GoogleMapsService::getApiScript( + $this->getLanguage()->getCode(), + [ 'libraries' => 'drawing' ] + ) + ); + + $outputPage->addModules( 'mapeditor' ); + $editorHtml = new MapEditorHtml( $this->getAttribs() ); + $html = $editorHtml->getEditorHtml(); + $outputPage->addHTML( $html ); + } + + /** + * @since 2.1 + * + * @return array + */ + protected function getAttribs() { + return [ + 'id' => 'map-canvas', + 'context' => 'Maps\MediaWiki\Specials\SpecialMapEditor' + ]; + } + + protected function getGroupName() { + return 'maps'; + } +} diff --git a/www/wiki/extensions/Maps/src/Presentation/CoordinateFormatter.php b/www/wiki/extensions/Maps/src/Presentation/CoordinateFormatter.php new file mode 100644 index 00000000..6b5d413d --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/CoordinateFormatter.php @@ -0,0 +1,36 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\Presentation; + +use DataValues\Geo\Formatters\LatLongFormatter; +use DataValues\Geo\Values\LatLongValue; +use ValueFormatters\FormatterOptions; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class CoordinateFormatter { + + private const PRECISION_MAP = [ + 'dms' => 1 / 360000, + 'dm' => 1 / 600000, + 'dd' => 1 / 1000000, + 'float' => 1 / 1000000, + ]; + + public function format( LatLongValue $latLong, string $format, bool $directional ) { + $formatter = new LatLongFormatter( new FormatterOptions( + [ + LatLongFormatter::OPT_FORMAT => $format, + LatLongFormatter::OPT_DIRECTIONAL => $directional, + LatLongFormatter::OPT_PRECISION => self::PRECISION_MAP[$format] + ] + ) ); + + return $formatter->format( $latLong ); + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/ElementJsonSerializer.php b/www/wiki/extensions/Maps/src/Presentation/ElementJsonSerializer.php new file mode 100644 index 00000000..6c4e33c5 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/ElementJsonSerializer.php @@ -0,0 +1,35 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\Presentation; + + +use Maps\Elements\BaseElement; + +class ElementJsonSerializer { + + private $parser; + + public function __construct( WikitextParser $parser ) { + $this->parser = $parser; + } + + public function elementToJson( BaseElement $element ): array { + $json = $element->getArrayValue(); + + $this->titleAndText( $json ); + + return $json; + } + + public function titleAndText( array &$elementJson ) { + $elementJson['title'] = $this->parser->wikitextToHtml( $elementJson['title'] ); + $elementJson['text'] = $this->parser->wikitextToHtml( $elementJson['text'] ); + + $hasTitleAndText = $elementJson['title'] !== '' && $elementJson['text'] !== ''; + $elementJson['text'] = ( $hasTitleAndText ? '<b>' . $elementJson['title'] . '</b>' : $elementJson['title'] ) . $elementJson['text']; + $elementJson['title'] = strip_tags( $elementJson['title'] ); + } + +}
\ No newline at end of file diff --git a/www/wiki/extensions/Maps/src/Presentation/KmlFormatter.php b/www/wiki/extensions/Maps/src/Presentation/KmlFormatter.php new file mode 100644 index 00000000..90df6d1e --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/KmlFormatter.php @@ -0,0 +1,78 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\Presentation; + +use Maps\Elements\Location; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class KmlFormatter { + + /** + * Builds and returns KML representing the set geographical objects. + */ + public function formatLocationsAsKml( Location ...$locations ): string { + $elements = $this->getKmlForLocations( $locations ); + + // http://earth.google.com/kml/2.2 + return <<<EOT +<?xml version="1.0" encoding="UTF-8"?> +<kml xmlns="http://www.opengis.net/kml/2.2"> + <Document> +$elements + </Document> +</kml> +EOT; + } + + private function getKmlForLocations( array $locations ): string { + return implode( + "\n", + array_map( + function( Location $location ) { + return $this->locationToKmlPlacemark( $location ); + }, + $locations + ) + ); + } + + + private function locationToKmlPlacemark( Location $location ): string { + // TODO: escaping? + $name = '<name><![CDATA[' . $location->getTitle() . ']]></name>'; + + // TODO: escaping? + $description = '<description><![CDATA[' . $location->getText() . ']]></description>'; + + $coordinates = '<coordinates>' + . $this->escapeValue( $this->getCoordinateString( $location ) ) + . '</coordinates>'; + + return <<<EOT + <Placemark> + $name + $description + <Point> + $coordinates + </Point> + </Placemark> +EOT; + } + + private function getCoordinateString( Location $location ): string { + // lon,lat[,alt] + return $location->getCoordinates()->getLongitude() + . ',' . $location->getCoordinates()->getLatitude() + . ',0'; + } + + private function escapeValue( string $value ): string { + return htmlspecialchars( $value, ENT_NOQUOTES ); + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/MapHtmlBuilder.php b/www/wiki/extensions/Maps/src/Presentation/MapHtmlBuilder.php new file mode 100644 index 00000000..aa47e558 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/MapHtmlBuilder.php @@ -0,0 +1,37 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\Presentation; + +use FormatJson; +use Html; + +class MapHtmlBuilder { + + public function getMapHTML( array $params, string $mapName, string $serviceName ): string { + if ( is_int( $params['height'] ) ) { + $params['height'] = (string)$params['height'] . 'px'; + } + + if ( is_int( $params['width'] ) ) { + $params['width'] = (string)$params['width'] . 'px'; + } + + return Html::rawElement( + 'div', + [ + 'id' => $mapName, + 'style' => "width: {$params['width']}; height: {$params['height']}; background-color: #cccccc; overflow: hidden;", + 'class' => 'maps-map maps-' . $serviceName + ], + wfMessage( 'maps-loading-map' )->inContentLanguage()->escaped() . + Html::element( + 'div', + [ 'style' => 'display:none', 'class' => 'mapdata' ], + FormatJson::encode( $params ) + ) + ); + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/MapsDistanceParser.php b/www/wiki/extensions/Maps/src/Presentation/MapsDistanceParser.php new file mode 100644 index 00000000..c8fb0ef1 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/MapsDistanceParser.php @@ -0,0 +1,133 @@ +<?php + +namespace Maps\Presentation; + +/** + * Static class for distance validation and parsing. Internal representations are in meters. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class MapsDistanceParser { + + private static $validatedDistanceUnit = false; + + private static $unitRegex = false; + + public static function parseAndFormat( string $distance, string $unit = null, int $decimals = 2 ): string { + return self::formatDistance( self::parseDistance( $distance ), $unit, $decimals ); + } + + /** + * Formats a given distance in meters to a distance in an optionally specified notation. + */ + public static function formatDistance( float $meters, string $unit = null, int $decimals = 2 ): string { + global $wgContLang; + $meters = $wgContLang->formatNum( round( $meters / self::getUnitRatio( $unit ), $decimals ) ); + return "$meters $unit"; + } + + /** + * Returns the unit to meter ratio in a safe way, by first resolving the unit. + */ + public static function getUnitRatio( string $unit = null ): float { + global $egMapsDistanceUnits; + return $egMapsDistanceUnits[self::getValidUnit( $unit )]; + } + + /** + * Returns a valid unit. If the provided one is invalid, the default will be used. + */ + public static function getValidUnit( string $unit = null ): string { + global $egMapsDistanceUnit, $egMapsDistanceUnits; + + // This ensures the value for $egMapsDistanceUnit is correct, and caches the result. + if ( self::$validatedDistanceUnit === false ) { + if ( !array_key_exists( $egMapsDistanceUnit, $egMapsDistanceUnits ) ) { + $units = array_keys( $egMapsDistanceUnits ); + $egMapsDistanceUnit = $units[0]; + } + + self::$validatedDistanceUnit = true; + } + + if ( $unit == null || !array_key_exists( $unit, $egMapsDistanceUnits ) ) { + $unit = $egMapsDistanceUnit; + } + + return $unit; + } + + /** + * Parses a distance optionally containing a unit to a float value in meters. + * + * @param string $distance + * + * @return float|false The distance in meters or false on failure + */ + public static function parseDistance( string $distance ) { + if ( !self::isDistance( $distance ) ) { + return false; + } + + $distance = self::normalizeDistance( $distance ); + + self::initUnitRegex(); + + $matches = []; + preg_match( '/^\d+(\.\d+)?\s?(' . self::$unitRegex . ')?$/', $distance, $matches ); + + $value = (float)( $matches[0] . $matches[1] ); + $value *= self::getUnitRatio( $matches[2] ); + + return $value; + } + + public static function isDistance( string $distance ): bool { + $distance = self::normalizeDistance( $distance ); + + self::initUnitRegex(); + + return (bool)preg_match( '/^\d+(\.\d+)?\s?(' . self::$unitRegex . ')?$/', $distance ); + } + + /** + * Normalizes a potential distance by removing spaces and turning comma's into dots. + */ + protected static function normalizeDistance( string $distance ): string { + $distance = trim( (string)$distance ); + $strlen = strlen( $distance ); + + for ( $i = 0; $i < $strlen; $i++ ) { + if ( !ctype_digit( $distance{$i} ) && !in_array( $distance{$i}, [ ',', '.' ] ) ) { + $value = substr( $distance, 0, $i ); + $unit = substr( $distance, $i ); + break; + } + } + + $value = str_replace( ',', '.', isset( $value ) ? $value : $distance ); + + if ( isset( $unit ) ) { + $value .= ' ' . str_replace( [ ' ', "\t" ], '', $unit ); + } + + return $value; + } + + private static function initUnitRegex() { + if ( self::$unitRegex === false ) { + global $egMapsDistanceUnits; + self::$unitRegex = implode( '|', array_keys( $egMapsDistanceUnits ) ) . '|'; + } + } + + /** + * Returns a list of all supported units. + */ + public static function getUnits(): array { + global $egMapsDistanceUnits; + return array_keys( $egMapsDistanceUnits ); + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/ParameterExtractor.php b/www/wiki/extensions/Maps/src/Presentation/ParameterExtractor.php new file mode 100644 index 00000000..33a93658 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/ParameterExtractor.php @@ -0,0 +1,47 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\Presentation; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class ParameterExtractor { + + /** + * Extracts the value of a parameter from a parameter list. + * + * @param string[] $parameterNames Name and aliases of the parameter. First match gets used + * @param string[] $rawParameters Parameters that did not get processed further than being put in a key-value map + * + * @return string|null + */ + public function extract( array $parameterNames, array $rawParameters ) { + foreach( $parameterNames as $parameterName ) { + foreach ( $rawParameters as $rawName => $rawValue ) { + if ( trim( strtolower( $rawName ) ) === $parameterName ) { + return trim( $rawValue ); + } + } + } + + return null; + } + + public static function extractFromKeyValueStrings( array $keyValueStrings ) { + $rawParameters = []; + + foreach ( $keyValueStrings as $keyValueString ) { + $parts = explode( '=', $keyValueString, 2 ); + + if ( count( $parts ) === 2 ) { + $rawParameters[$parts[0]] = $parts[1]; + } + } + + return $rawParameters; + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/WikitextParser.php b/www/wiki/extensions/Maps/src/Presentation/WikitextParser.php new file mode 100644 index 00000000..e0c2601a --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/WikitextParser.php @@ -0,0 +1,30 @@ +<?php + +declare( strict_types = 1 ); + +namespace Maps\Presentation; + +use Parser; +use ParserOptions; + +class WikitextParser { + + private $parser; + + public function __construct( Parser $parser ) { + $this->parser = $parser; + } + + public function wikitextToHtml( string $text ): string { + if ( trim( $text ) === '' ) { + return ''; + } + + return $this->parser->parse( + $text, + $this->parser->getTitle(), + new ParserOptions() + )->getText(); + } + +}
\ No newline at end of file diff --git a/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/CircleParser.php b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/CircleParser.php new file mode 100644 index 00000000..110064c0 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/CircleParser.php @@ -0,0 +1,86 @@ +<?php + +namespace Maps\Presentation\WikitextParsers; + +use DataValues\Geo\Values\LatLongValue; +use Jeroen\SimpleGeocoder\Geocoder; +use Maps\Elements\Circle; +use Maps\MapsFactory; +use ValueParsers\ParseException; +use ValueParsers\StringValueParser; +use ValueParsers\ValueParser; + +/** + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Kim Eik + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class CircleParser implements ValueParser { + + private $metaDataSeparator = '~'; + + private $geocoder; + + public function __construct( $geocoder = null ) { + $this->geocoder = $geocoder instanceof Geocoder ? $geocoder : MapsFactory::newDefault()->getGeocoder(); + } + + /** + * @see StringValueParser::stringParse + * + * @since 3.0 + * + * @param string $value + * + * @return Circle + */ + public function parse( $value ) { + $metaData = explode( $this->metaDataSeparator, $value ); + $circleData = explode( ':', array_shift( $metaData ) ); + + $circle = new Circle( $this->stringToLatLongValue( $circleData[0] ), (float)$circleData[1] ); + + if ( $metaData !== [] ) { + $circle->setTitle( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $circle->setText( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $circle->setStrokeColor( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $circle->setStrokeOpacity( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $circle->setStrokeWeight( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $circle->setFillColor( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $circle->setFillOpacity( array_shift( $metaData ) ); + } + + return $circle; + } + + private function stringToLatLongValue( string $location ): LatLongValue { + $latLong = $this->geocoder->geocode( $location ); + + if ( $latLong === null ) { + throw new ParseException( 'Failed to parse or geocode' ); + } + + return $latLong; + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/DistanceParser.php b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/DistanceParser.php new file mode 100644 index 00000000..2f90e9fc --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/DistanceParser.php @@ -0,0 +1,38 @@ +<?php + +namespace Maps\Presentation\WikitextParsers; + +use ValueParsers\ParseException; +use ValueParsers\StringValueParser; + +/** + * ValueParser that parses the string representation of a distance. + * + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class DistanceParser extends StringValueParser { + + /** + * @see StringValueParser::stringParse + * + * @since 3.0 + * + * @param string $value + * + * @return float + * @throws ParseException + */ + public function stringParse( $value ) { + $distance = \Maps\Presentation\MapsDistanceParser::parseDistance( $value ); + + if ( is_float( $distance ) ) { + return $distance; + } + + throw new ParseException( 'Not a distance' ); + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/ImageOverlayParser.php b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/ImageOverlayParser.php new file mode 100644 index 00000000..c2d81591 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/ImageOverlayParser.php @@ -0,0 +1,83 @@ +<?php + +namespace Maps\Presentation\WikitextParsers; + +use DataValues\Geo\Values\LatLongValue; +use Jeroen\SimpleGeocoder\Geocoder; +use Maps\Elements\ImageOverlay; +use Maps\MapsFactory; +use ValueParsers\ParseException; +use ValueParsers\ValueParser; + +/** + * @since 3.1 + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class ImageOverlayParser implements ValueParser { + + private $geocoder; + + public function __construct( $geocoder = null ) { + $this->geocoder = $geocoder instanceof Geocoder ? $geocoder : MapsFactory::newDefault()->getGeocoder(); + } + + /** + * @since 3.1 + * + * @param string $value + * + * @return ImageOverlay + * @throws ParseException + */ + public function parse( $value ) { + $metaData = explode( '~', $value ); + $imageParameters = explode( ':', array_shift( $metaData ), 3 ); + + if ( count( $imageParameters ) !== 3 ) { + throw new ParseException( 'Need 3 parameters for an image overlay' ); + } + + $boundsNorthEast = $this->stringToLatLongValue( $imageParameters[0] ); + $boundsSouthWest = $this->stringToLatLongValue( $imageParameters[1] ); + $imageUrl = \Maps\MapsFunctions::getFileUrl( $imageParameters[2] ); + + $overlay = new ImageOverlay( $boundsNorthEast, $boundsSouthWest, $imageUrl ); + + if ( $metaData !== [] ) { + $overlay->setTitle( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $overlay->setText( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $overlay->setLink( $this->getUrlFromLinkString( array_shift( $metaData ) ) ); + } + + return $overlay; + } + + private function getUrlFromLinkString( string $linkString ): string { + $linkPrefix = 'link:'; + + if ( substr( $linkString, 0, strlen( $linkPrefix ) ) === $linkPrefix ) { + return substr( $linkString, strlen( $linkPrefix ) ); + } + + return $linkString; + } + + private function stringToLatLongValue( string $location ): LatLongValue { + $latLong = $this->geocoder->geocode( $location ); + + if ( $latLong === null ) { + throw new ParseException( 'Failed to parse or geocode' ); + } + + return $latLong; + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/LineParser.php b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/LineParser.php new file mode 100644 index 00000000..4e25a1f7 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/LineParser.php @@ -0,0 +1,163 @@ +<?php + +namespace Maps\Presentation\WikitextParsers; + +use DataValues\Geo\Values\LatLongValue; +use Jeroen\SimpleGeocoder\Geocoder; +use Maps\Elements\Line; +use Maps\MapsFactory; +use ValueParsers\StringValueParser; +use ValueParsers\ValueParser; + +/** + * ValueParser that parses the string representation of a line. + * + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Kim Eik + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class LineParser implements ValueParser { + + private $metaDataSeparator = '~'; + + private $geocoder = null; + + public function setGeocoder( Geocoder $geocoder ) { + $this->geocoder = $geocoder; + } + + private function getGeocoder(): Geocoder { + if ( $this->geocoder == null ) { + $this->geocoder = MapsFactory::newDefault()->getGeocoder(); + } + + return $this->geocoder; + } + + /** + * @see StringValueParser::stringParse + * + * @since 3.0 + * + * @param string $value + * + * @return Line + */ + public function parse( $value ) { + $parts = explode( $this->metaDataSeparator, $value ); + + $line = $this->constructShapeFromLatLongValues( + $this->parseCoordinates( + explode( ':', array_shift( $parts ) ) + ) + ); + + $this->handleCommonParams( $parts, $line ); + + return $line; + } + + protected function constructShapeFromLatLongValues( array $locations ) { + return new Line( $locations ); + } + + /** + * @since 3.0 + * + * @param string[] $coordinateStrings + * + * @return LatLongValue[] + */ + protected function parseCoordinates( array $coordinateStrings ): array { + $coordinates = []; + + foreach ( $coordinateStrings as $coordinateString ) { + $coordinate = $this->getGeocoder()->geocode( $coordinateString ); + + if ( $coordinate === null ) { + // TODO: good if the user knows something has been omitted + } else { + $coordinates[] = $coordinate; + } + } + + return $coordinates; + } + + /** + * This method requires that parameters are positionally correct, + * 1. Link (one parameter) or bubble data (two parameters) + * 2. Stroke data (three parameters) + * 3. Fill data (two parameters) + * e.g ...title~text~strokeColor~strokeOpacity~strokeWeight~fillColor~fillOpacity + * + * @since 3.0 + * + * @param array $params + * @param Line $line + */ + protected function handleCommonParams( array &$params, Line &$line ) { + //Handle bubble and link parameters + + //create link data + $linkOrTitle = array_shift( $params ); + if ( $link = $this->isLinkParameter( $linkOrTitle ) ) { + $this->setLinkFromParameter( $line, $link ); + } else { + //create bubble data + $this->setBubbleDataFromParameter( $line, $params, $linkOrTitle ); + } + + //handle stroke parameters + if ( $color = array_shift( $params ) ) { + $line->setStrokeColor( $color ); + } + + if ( $opacity = array_shift( $params ) ) { + $line->setStrokeOpacity( $opacity ); + } + + if ( $weight = array_shift( $params ) ) { + $line->setStrokeWeight( $weight ); + } + } + + /** + * Checks if a string is prefixed with link: + * + * @static + * + * @param $link + * + * @return bool|string + * @since 2.0 + */ + private function isLinkParameter( $link ) { + if ( strpos( $link, 'link:' ) === 0 ) { + return substr( $link, 5 ); + } + + return false; + } + + protected function setLinkFromParameter( Line &$line, $link ) { + if ( filter_var( $link, FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED ) ) { + $line->setLink( $link ); + } else { + $title = \Title::newFromText( $link ); + $line->setLink( $title->getFullURL() ); + } + } + + protected function setBubbleDataFromParameter( Line &$line, &$params, $title ) { + if ( $title ) { + $line->setTitle( $title ); + } + if ( $text = array_shift( $params ) ) { + $line->setText( $text ); + } + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/LocationParser.php b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/LocationParser.php new file mode 100644 index 00000000..26af76ef --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/LocationParser.php @@ -0,0 +1,147 @@ +<?php + +namespace Maps\Presentation\WikitextParsers; + +use DataValues\Geo\Parsers\LatLongParser; +use Jeroen\SimpleGeocoder\Geocoder; +use Maps\Elements\Location; +use Maps\FileUrlFinder; +use Maps\MapsFactory; +use Title; +use ValueParsers\ParseException; +use ValueParsers\StringValueParser; +use ValueParsers\ValueParser; + +/** + * ValueParser that parses the string representation of a location. + * + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class LocationParser implements ValueParser { + + private $geocoder; + private $fileUrlFinder; + private $useAddressAsTitle; + + /** + * @deprecated Use newInstance instead + */ + public function __construct( $enableLegacyCrud = true ) { + if ( $enableLegacyCrud ) { + $this->geocoder = MapsFactory::globalInstance()->getGeocoder(); + $this->fileUrlFinder = MapsFactory::globalInstance()->getFileUrlFinder(); + $this->useAddressAsTitle = false; + } + } + + public static function newInstance( Geocoder $geocoder, FileUrlFinder $fileUrlFinder, bool $useAddressAsTitle = false ): self { + $instance = new self( false ); + $instance->geocoder = $geocoder; + $instance->fileUrlFinder = $fileUrlFinder; + $instance->useAddressAsTitle = $useAddressAsTitle; + return $instance; + } + + /** + * @see StringValueParser::stringParse + * + * @since 3.0 + * + * @param string $value + * + * @return Location + * @throws ParseException + */ + public function parse( $value ) { + $separator = '~'; + + $metaData = explode( $separator, $value ); + + $coordinatesOrAddress = array_shift( $metaData ); + $coordinates = $this->geocoder->geocode( $coordinatesOrAddress ); + + if ( $coordinates === null ) { + throw new ParseException( 'Location is not a parsable coordinate and not a geocodable address' ); + } + + $location = new Location( $coordinates ); + + if ( $metaData !== [] ) { + $this->setTitleOrLink( $location, array_shift( $metaData ) ); + } else { + if ( $this->useAddressAsTitle && $this->isAddress( $coordinatesOrAddress ) ) { + $location->setTitle( $coordinatesOrAddress ); + } + } + + if ( $metaData !== [] ) { + $location->setText( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $location->setIcon( $this->fileUrlFinder->getUrlForFileName( array_shift( $metaData ) ) ); + } + + if ( $metaData !== [] ) { + $location->setGroup( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $location->setInlineLabel( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $location->setVisitedIcon( $this->fileUrlFinder->getUrlForFileName( array_shift( $metaData ) ) ) ; + } + + return $location; + } + + private function setTitleOrLink( Location $location, $titleOrLink ) { + if ( $this->isLink( $titleOrLink ) ) { + $this->setLink( $location, $titleOrLink ); + } else { + $location->setTitle( $titleOrLink ); + } + } + + private function isLink( $value ) { + return strpos( $value, 'link:' ) === 0; + } + + private function setLink( Location $location, $link ) { + $link = substr( $link, 5 ); + $location->setLink( $this->getExpandedLink( $link ) ); + } + + private function getExpandedLink( $link ) { + if ( filter_var( $link, FILTER_VALIDATE_URL ) ) { + return $link; + } + + $title = Title::newFromText( $link ); + + if ( $title === null ) { + return ''; + } + + return $title->getFullURL(); + } + + private function isAddress( string $coordsOrAddress ): bool { + $coordinateParser = new LatLongParser(); + + try { + $coordinateParser->parse( $coordsOrAddress ); + } + catch ( ParseException $parseException ) { + return true; + } + + return false; + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/PolygonParser.php b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/PolygonParser.php new file mode 100644 index 00000000..1bf14a7f --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/PolygonParser.php @@ -0,0 +1,41 @@ +<?php + +namespace Maps\Presentation\WikitextParsers; + +use Maps\Elements\Line; +use Maps\Elements\Polygon; + +/** + * ValueParser that parses the string representation of a polygon. + * + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class PolygonParser extends LineParser { + + protected function constructShapeFromLatLongValues( array $locations ) { + return new Polygon( $locations ); + } + + protected function handleCommonParams( array &$params, Line &$line ) { + parent::handleCommonParams( $params, $line ); + $this->handlePolygonParams( $params, $line ); + } + + protected function handlePolygonParams( array &$params, Polygon &$polygon ) { + if ( $fillColor = array_shift( $params ) ) { + $polygon->setFillColor( $fillColor ); + } + + if ( $fillOpacity = array_shift( $params ) ) { + $polygon->setFillOpacity( $fillOpacity ); + } + + if ( $showOnlyOnHover = array_shift( $params ) ) { + $polygon->setOnlyVisibleOnHover( strtolower( trim( $showOnlyOnHover ) ) === 'on' ); + } + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/RectangleParser.php b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/RectangleParser.php new file mode 100644 index 00000000..1b694a27 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/RectangleParser.php @@ -0,0 +1,89 @@ +<?php + +namespace Maps\Presentation\WikitextParsers; + +use DataValues\Geo\Values\LatLongValue; +use Jeroen\SimpleGeocoder\Geocoder; +use Maps\Elements\Rectangle; +use Maps\MapsFactory; +use ValueParsers\ParseException; +use ValueParsers\StringValueParser; +use ValueParsers\ValueParser; + +/** + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Kim Eik + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class RectangleParser implements ValueParser { + + private $metaDataSeparator = '~'; + + private $geocoder; + + public function __construct( $geocoder = null ) { + $this->geocoder = $geocoder instanceof Geocoder ? $geocoder : MapsFactory::newDefault()->getGeocoder(); + } + + /** + * @see StringValueParser::stringParse + * + * @since 3.0 + * + * @param string $value + * + * @return Rectangle + */ + public function parse( $value ) { + $metaData = explode( $this->metaDataSeparator, $value ); + $rectangleData = explode( ':', array_shift( $metaData ) ); + + $rectangle = new Rectangle( + $this->stringToLatLongValue( $rectangleData[0] ), + $this->stringToLatLongValue( $rectangleData[1] ) + ); + + if ( $metaData !== [] ) { + $rectangle->setTitle( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $rectangle->setText( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $rectangle->setStrokeColor( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $rectangle->setStrokeOpacity( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $rectangle->setStrokeWeight( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $rectangle->setFillColor( array_shift( $metaData ) ); + } + + if ( $metaData !== [] ) { + $rectangle->setFillOpacity( array_shift( $metaData ) ); + } + + return $rectangle; + } + + private function stringToLatLongValue( string $location ): LatLongValue { + $latLong = $this->geocoder->geocode( $location ); + + if ( $latLong === null ) { + throw new ParseException( 'Failed to parse or geocode' ); + } + + return $latLong; + } + +} diff --git a/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/WmsOverlayParser.php b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/WmsOverlayParser.php new file mode 100644 index 00000000..b2b784b9 --- /dev/null +++ b/www/wiki/extensions/Maps/src/Presentation/WikitextParsers/WmsOverlayParser.php @@ -0,0 +1,49 @@ +<?php + +namespace Maps\Presentation\WikitextParsers; + +use Maps\Elements\WmsOverlay; +use ValueParsers\ParseException; +use ValueParsers\ValueParser; + +/** + * ValueParser that parses the string representation of a WMS layer + * + * @since 3.0 + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class WmsOverlayParser implements ValueParser { + + /** + * Parses the provided string and returns the result. + * + * @since 3.0 + * + * @param string $value + * + * @return WmsOverlay + * @throws ParseException + */ + public function parse( $value ) { + if ( !is_string( $value ) ) { + throw new ParseException( 'Not a string' ); + } + + $separator = " "; + $metaData = explode( $separator, $value ); + + if ( count( $metaData ) >= 2 ) { + $wmsOverlay = new WmsOverlay( $metaData[0], $metaData[1] ); + if ( count( $metaData ) == 3 ) { + $wmsOverlay->setWmsStyleName( $metaData[2] ); + } + + return $wmsOverlay; + } + + throw new ParseException( 'Need at least two parameters, url to WMS server and map layer name' ); + } + +} diff --git a/www/wiki/extensions/Maps/src/SemanticMW/DataValues/CoordinateValue.php b/www/wiki/extensions/Maps/src/SemanticMW/DataValues/CoordinateValue.php new file mode 100644 index 00000000..47de2632 --- /dev/null +++ b/www/wiki/extensions/Maps/src/SemanticMW/DataValues/CoordinateValue.php @@ -0,0 +1,266 @@ +<?php + +namespace Maps\SemanticMW\DataValues; + +use DataValues\Geo\Parsers\LatLongParser; +use DataValues\Geo\Values\LatLongValue; +use InvalidArgumentException; +use Maps\MapsFactory; +use Maps\Presentation\MapsDistanceParser; +use SMW\Query\Language\Description; +use SMW\Query\Language\ThingDescription; +use SMW\Query\QueryComparator; +use SMWDataItem; +use SMWDataValue; +use SMWDIGeoCoord; +use SMWOutputs; +use ValueParsers\ParseException; + +/** + * @property SMWDIGeoCoord m_dataitem + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @author Markus Krötzsch + */ +class CoordinateValue extends SMWDataValue { + + private $wikiValue; + + /** + * Overwrite SMWDataValue::getQueryDescription() to be able to process + * comparators between all values. + * + * @param string $value + * + * @return Description + * @throws InvalidArgumentException + */ + public function getQueryDescription( $value ) { + if ( !is_string( $value ) ) { + throw new InvalidArgumentException( '$value needs to be a string' ); + } + + list( $distance, $comparator ) = $this->parseUserValue( $value ); + $distance = $this->parserDistance( $distance ); + + $this->setUserValue( $value ); + + switch ( true ) { + case !$this->isValid(): + return new ThingDescription(); + case $distance !== false: + return new \Maps\SemanticMW\ValueDescriptions\AreaDescription( + $this->getDataItem(), + $comparator, + $distance + ); + default: + return new \Maps\SemanticMW\ValueDescriptions\CoordinateDescription( + $this->getDataItem(), + null, + $comparator + ); + } + } + + /** + * @see SMWDataValue::parseUserValue + */ + protected function parseUserValue( $value ) { + if ( !is_string( $value ) ) { + throw new InvalidArgumentException( '$value needs to be a string' ); + } + + $this->wikiValue = $value; + + $comparator = SMW_CMP_EQ; + $distance = false; + + if ( $value === '' ) { + $this->addError( wfMessage( 'smw_novalues' )->text() ); + } else { + $comparator = QueryComparator::getInstance()->extractComparatorFromString( $value ); + + list( $coordinates, $distance ) = $this->findValueParts( $value ); + + $this->tryParseAndSetDataItem( $coordinates ); + } + + return [ $distance, $comparator ]; + } + + private function findValueParts( string $value ): array { + $parts = explode( '(', $value ); + + $coordinates = trim( array_shift( $parts ) ); + $distance = count( $parts ) > 0 ? trim( array_shift( $parts ) ) : false; + + return [ $coordinates, $distance ]; + } + + private function tryParseAndSetDataItem( string $coordinates ) { + $parser = new LatLongParser(); + + try { + $value = $parser->parse( $coordinates ); + $this->m_dataitem = new SMWDIGeoCoord( $value->getLatitude(), $value->getLongitude() ); + } + catch ( ParseException $parseException ) { + $this->addError( wfMessage( 'maps_unrecognized_coords', $coordinates, 1 )->text() ); + + // Make sure this is always set + // TODO: Why is this needed?! + $this->m_dataitem = new SMWDIGeoCoord( [ 'lat' => 0, 'lon' => 0 ] ); + } + } + + private function parserDistance( $distance ) { + if ( $distance !== false ) { + $distance = substr( trim( $distance ), 0, -1 ); + + if ( !MapsDistanceParser::isDistance( $distance ) ) { + $this->addError( wfMessage( 'semanticmaps-unrecognizeddistance', $distance )->text() ); + $distance = false; + } + } + + return $distance; + } + + /** + * @see SMWDataValue::getShortHTMLText + * + * @since 0.6 + */ + public function getShortHTMLText( $linker = null ) { + return $this->getShortWikiText( $linker ); + } + + /** + * @see SMWDataValue::getShortWikiText + */ + public function getShortWikiText( $linked = null ) { + if ( $this->isValid() ) { + if ( $this->m_caption === false ) { + return $this->getFormattedCoord( $this->m_dataitem ); + } + + return $this->m_caption; + } + + return $this->getErrorText(); + } + + /** + * @param SMWDIGeoCoord $dataItem + * @param string|null $format + * + * @return string|null + */ + private function getFormattedCoord( SMWDIGeoCoord $dataItem, string $format = null ) { + return MapsFactory::globalInstance()->getCoordinateFormatter()->format( + new LatLongValue( + $dataItem->getLatitude(), + $dataItem->getLongitude() + ), + $format ?? $GLOBALS['smgQPCoodFormat'], + $GLOBALS['smgQPCoodDirectional'] + ); + } + + /** + * @see SMWDataValue::getLongHTMLText + */ + public function getLongHTMLText( $linker = null ) { + return $this->getLongWikiText( $linker ); + } + + /** + * @see SMWDataValue::getLongWikiText + * + * @since 0.6 + */ + public function getLongWikiText( $linked = null ) { + if ( $this->isValid() ) { + SMWOutputs::requireHeadItem( SMW_HEADER_TOOLTIP ); + + // TODO: fix lang keys so they include the space and coordinates. + $coordinateSet = $this->m_dataitem->getCoordinateSet(); + + $text = $this->getFormattedCoord( $this->m_dataitem ); + + $lines = [ + wfMessage( 'semanticmaps-latitude', $coordinateSet['lat'] )->inContentLanguage()->escaped(), + wfMessage( 'semanticmaps-longitude', $coordinateSet['lon'] )->inContentLanguage()->escaped(), + ]; + + if ( array_key_exists( 'alt', $coordinateSet ) ) { + $lines[] = wfMessage( 'semanticmaps-altitude', $coordinateSet['alt'] )->inContentLanguage()->escaped(); + } + + return '<span class="smwttinline">' . htmlspecialchars( $text ) . '<span class="smwttcontent">' . + implode( '<br />', $lines ) . + '</span></span>'; + } else { + return $this->getErrorText(); + } + } + + /** + * @see SMWDataValue::getWikiValue + */ + public function getWikiValue() { + return $this->wikiValue; + } + + /** + * @see SMWDataValue::setDataItem + * + * @param SMWDataItem $dataItem + * + * @return boolean + */ + protected function loadDataItem( SMWDataItem $dataItem ) { + if ( $dataItem instanceof SMWDIGeoCoord ) { + $formattedValue = $this->getFormattedCoord( $dataItem ); + + if ( $formattedValue !== null ) { + $this->wikiValue = $formattedValue; + $this->m_dataitem = $dataItem; + return true; + } + } + + return false; + } + + /** + * Create links to mapping services based on a wiki-editable message. The parameters + * available to the message are: + * + * $1: The location in non-directional float notation. + * $2: The location in directional DMS notation. + * $3: The latitude in non-directional float notation. + * $4 The longitude in non-directional float notation. + * + * @return array + */ + protected function getServiceLinkParams() { + $coordinateSet = $this->m_dataitem->getCoordinateSet(); + return [ + $this->getFormattedCoord( $this->m_dataitem, 'float' ), // TODO + $this->getFormattedCoord( $this->m_dataitem, 'dms' ), // TODO + $coordinateSet['lat'], + $coordinateSet['lon'] + ]; + } + + /** + * @return SMWDIGeoCoord|\SMWDIError + */ + public function getDataItem() { + return parent::getDataItem(); + } + +} diff --git a/www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/KmlPrinter.php b/www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/KmlPrinter.php new file mode 100644 index 00000000..1bda31c6 --- /dev/null +++ b/www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/KmlPrinter.php @@ -0,0 +1,146 @@ +<?php + +namespace Maps\SemanticMW\ResultPrinters; + +use Maps\Presentation\KmlFormatter; +use ParamProcessor\ParamDefinition; +use SMW\Query\ResultPrinters\FileExportPrinter; +use SMWQueryResult; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class KmlPrinter extends FileExportPrinter { + + /** + * @param SMWQueryResult $res + * @param int $outputMode + * + * @return string + */ + public function getResultText( SMWQueryResult $res, $outputMode ) { + if ( $outputMode == SMW_OUTPUT_FILE ) { + return $this->getKML( $res, $outputMode ); + } + + return $this->getKMLLink( $res, $outputMode ); + } + + private function getKML( SMWQueryResult $res, int $outputMode ): string { + $queryHandler = new QueryHandler( $res, $outputMode, $this->params['linkabsolute'] ); + $queryHandler->setText( $this->params['text'] ); + $queryHandler->setTitle( $this->params['title'] ); + $queryHandler->setSubjectSeparator( '' ); + + $formatter = new KmlFormatter(); + return $formatter->formatLocationsAsKml( ...$queryHandler->getLocations() ); + } + + /** + * Returns a link (HTML) pointing to a query that returns the actual KML file. + */ + private function getKMLLink( SMWQueryResult $res, int $outputMode ): string { + $searchLabel = $this->getSearchLabel( $outputMode ); + $link = $res->getQueryLink( + $searchLabel ? $searchLabel : wfMessage( 'semanticmaps-kml-link' )->inContentLanguage()->text() + ); + $link->setParameter( 'kml', 'format' ); + $link->setParameter( $this->params['linkabsolute'] ? 'yes' : 'no', 'linkabsolute' ); + + if ( $this->params['title'] !== '' ) { + $link->setParameter( $this->params['title'], 'title' ); + } + + if ( $this->params['text'] !== '' ) { + $link->setParameter( $this->params['text'], 'text' ); + } + + // Fix for offset-error in getQueryLink() + // (getQueryLink by default sets offset to point to the next + // result set, fix by setting it to 0 if now explicitly set) + if ( array_key_exists( 'offset', $this->params ) ) { + $link->setParameter( $this->params['offset'], 'offset' ); + } else { + $link->setParameter( 0, 'offset' ); + } + + if ( array_key_exists( 'limit', $this->params ) ) { + $link->setParameter( $this->params['limit'], 'limit' ); + } else { // Use a reasonable default limit. + $link->setParameter( 20, 'limit' ); + } + + $this->isHTML = ( $outputMode == SMW_OUTPUT_HTML ); + + return $link->getText( $outputMode, $this->mLinker ); + } + + /** + * @see SMWResultPrinter::getParamDefinitions + * + * @param ParamDefinition[] $definitions + * + * @return array + */ + public function getParamDefinitions( array $definitions ) { + global $egMapsDefaultLabel, $egMapsDefaultTitle; + + $definitions['text'] = [ + 'message' => 'semanticmaps-kml-text', + 'default' => $egMapsDefaultLabel, + ]; + + $definitions['title'] = [ + 'message' => 'semanticmaps-kml-title', + 'default' => $egMapsDefaultTitle, + ]; + + $definitions['linkabsolute'] = [ + 'message' => 'semanticmaps-kml-linkabsolute', + 'type' => 'boolean', + 'default' => true, + ]; + + return $definitions; + } + + /** + * @see SMWIExportPrinter::getMimeType + * + * @param SMWQueryResult $queryResult + * + * @return string + */ + public function getMimeType( SMWQueryResult $queryResult ) { + return 'application/vnd.google-earth.kml+xml'; + } + + /** + * @see SMWIExportPrinter::getFileName + * + * @param SMWQueryResult $queryResult + * + * @return string|boolean + */ + public function getFileName( SMWQueryResult $queryResult ) { + return 'kml.kml'; + } + + /** + * @see SMWResultPrinter::getName() + */ + public final function getName() { + return wfMessage( 'semanticmaps-kml' )->text(); + } + + /** + * @see SMWResultPrinter::handleParameters + * + * @param array $params + * @param $outputMode + */ + protected function handleParameters( array $params, $outputMode ) { + $this->params = $params; + } +} diff --git a/www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/MapPrinter.php b/www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/MapPrinter.php new file mode 100644 index 00000000..6ab945f6 --- /dev/null +++ b/www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/MapPrinter.php @@ -0,0 +1,403 @@ +<?php + +namespace Maps\SemanticMW\ResultPrinters; + +use FormatJson; +use Html; +use Linker; +use Maps\Elements\BaseElement; +use Maps\Elements\Location; +use Maps\FileUrlFinder; +use Maps\MappingService; +use Maps\MapsFunctions; +use Maps\Presentation\ElementJsonSerializer; +use Maps\Presentation\MapHtmlBuilder; +use Maps\Presentation\WikitextParser; +use Maps\Presentation\WikitextParsers\LocationParser; +use ParamProcessor\ParamDefinition; +use Parser; +use SMW\Query\ResultPrinters\ResultPrinter; +use SMWOutputs; +use SMWQueryResult; +use Title; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + * @author Peter Grassberger < petertheone@gmail.com > + */ +class MapPrinter extends ResultPrinter { + + private static $services = []; + + /** + * @var LocationParser + */ + private $locationParser; + + /** + * @var FileUrlFinder + */ + private $fileUrlFinder; + + /** + * @var MappingService + */ + private $service; + + /** + * @var WikitextParser + */ + private $wikitextParser; + + /** + * @var ElementJsonSerializer + */ + private $elementSerializer; + + /** + * @var string|boolean + */ + private $fatalErrorMsg = false; + + /** + * @param string $format + * @param bool $inline + */ + public function __construct( $format, $inline = true ) { + $this->service = self::$services[$format]; + + parent::__construct( $format, $inline ); + } + + /** + * @since 3.4 + * FIXME: this is a temporary hack that should be replaced when SMW allows for dependency + * injection in query printers. + * + * @param MappingService $service + */ + public static function registerService( MappingService $service ) { + self::$services[$service->getName()] = $service; + } + + public static function registerDefaultService( $serviceName ) { + self::$services['map'] = self::$services[$serviceName]; + } + + private function getParser(): Parser { + $parser = $GLOBALS['wgParser']; + + if ( $parser instanceof \StubObject ) { + return $parser->_newObject(); + } + + return $parser; + } + + private function getParserClone(): Parser { + $parser = $this->getParser(); + return clone $parser; + } + + /** + * Builds up and returns the HTML for the map, with the queried coordinate data on it. + * + * @param SMWQueryResult $res + * @param int $outputMode + * + * @return string + */ + public final function getResultText( SMWQueryResult $res, $outputMode ) { + if ( $this->fatalErrorMsg !== false ) { + return $this->fatalErrorMsg; + } + + $factory = \Maps\MapsFactory::newDefault(); + $this->locationParser = $factory->newLocationParser(); + $this->fileUrlFinder = $factory->getFileUrlFinder(); + + $this->wikitextParser = new WikitextParser( $this->getParserClone() ); + $this->elementSerializer = new ElementJsonSerializer( $this->wikitextParser ); + + $this->addTrackingCategoryIfNeeded(); + + $params = $this->params; + + $queryHandler = new QueryHandler( $res, $outputMode ); + $queryHandler->setLinkStyle( $params['link'] ); + $queryHandler->setHeaderStyle( $params['headers'] ); + $queryHandler->setShowSubject( $params['showtitle'] ); + $queryHandler->setTemplate( $params['template'] ); + $queryHandler->setUserParam( $params['userparam'] ); + $queryHandler->setHideNamespace( $params['hidenamespace'] ); + $queryHandler->setActiveIcon( $params['activeicon'] ); + + $this->handleMarkerData( $params, $queryHandler ); + + $params['lines'] = $this->elementsToJson( $params['lines'] ); + $params['polygons'] = $this->elementsToJson( $params['polygons'] ); + $params['circles'] = $this->elementsToJson( $params['circles'] ); + $params['rectangles'] = $this->elementsToJson( $params['rectangles'] ); + + $params['ajaxquery'] = urlencode( $params['ajaxquery'] ); + + if ( $params['locations'] === [] ) { + return $params['default']; + } + + // We can only take care of the zoom defaulting here, + // as not all locations are available in whats passed to Validator. + if ( $this->fullParams['zoom']->wasSetToDefault() && count( $params['locations'] ) > 1 ) { + $params['zoom'] = false; + } + + $mapId = $this->service->newMapId(); + + SMWOutputs::requireHeadItem( + $mapId, + $this->service->getDependencyHtml( $params ) + ); + + foreach ( $this->service->getResourceModules() as $resourceModule ) { + SMWOutputs::requireResource( $resourceModule ); + } + + if ( array_key_exists( 'source', $params ) ) { + unset( $params['source'] ); + } + + return ( new MapHtmlBuilder() )->getMapHTML( + $params, + $mapId, + $this->service->getName() + ); + } + + private function elementsToJson( array $elements ) { + return array_map( + function( BaseElement $element ) { + return $this->elementSerializer->elementToJson( $element ); + }, + $elements + ); + } + + private function addTrackingCategoryIfNeeded() { + /** + * @var Parser $wgParser + */ + global $wgParser; + + if ( $GLOBALS['egMapsEnableCategory'] && $wgParser->getOutput() !== null ) { + $wgParser->addTrackingCategory( 'maps-tracking-category' ); + } + } + + /** + * Converts the data in the coordinates parameter to JSON-ready objects. + * These get stored in the locations parameter, and the coordinates on gets deleted. + * + * @param array &$params + * @param QueryHandler $queryHandler + */ + private function handleMarkerData( array &$params, QueryHandler $queryHandler ) { + $params['centre'] = $this->getCenter( $params['centre'] ); + + $iconUrl = $this->fileUrlFinder->getUrlForFileName( $params['icon'] ); + $visitedIconUrl = $this->fileUrlFinder->getUrlForFileName( $params['visitedicon'] ); + + $params['locations'] = $this->getJsonForStaticLocations( + $params['staticlocations'], + $params, + $iconUrl, + $visitedIconUrl + ); + + unset( $params['staticlocations'] ); + + $params['locations'] = array_merge( + $params['locations'], + $this->getJsonForLocations( + $queryHandler->getLocations(), + $params, + $iconUrl, + $visitedIconUrl + ) + ); + } + + private function getCenter( $coordinatesOrAddress ) { + if ( $coordinatesOrAddress === false ) { + return false; + } + + try { + // FIXME: a Location makes no sense here, since the non-coordinate data is not used + $location = $this->locationParser->parse( $coordinatesOrAddress ); + } + catch ( \Exception $ex ) { + // TODO: somehow report this to the user + return false; + } + + return $location->getJSONObject(); + } + + private function getJsonForStaticLocations( array $staticLocations, array $params, $iconUrl, $visitedIconUrl ) { + $locationsJson = []; + + foreach ( $staticLocations as $location ) { + $locationsJson[] = $this->getJsonForStaticLocation( + $location, + $params, + $iconUrl, + $visitedIconUrl + ); + } + + return $locationsJson; + } + + private function getJsonForStaticLocation( Location $location, array $params, $iconUrl, $visitedIconUrl ) { + $jsonObj = $location->getJSONObject( $params['title'], $params['label'], $iconUrl, '', '', $visitedIconUrl ); + + $this->elementSerializer->titleAndText( $jsonObj ); + + if ( $params['pagelabel'] ) { + $jsonObj['inlineLabel'] = Linker::link( Title::newFromText( $jsonObj['title'] ) ); + } + + return $jsonObj; + } + + /** + * @param Location[] $locations + * @param array $params + * @param string $iconUrl + * @param string $visitedIconUrl + * + * @return array + */ + private function getJsonForLocations( iterable $locations, array $params, string $iconUrl, string $visitedIconUrl ): array { + $locationsJson = []; + + foreach ( $locations as $location ) { + $jsonObj = $location->getJSONObject( + $params['title'], + $params['label'], + $iconUrl, + '', + '', + $visitedIconUrl + ); + + $jsonObj['title'] = strip_tags( $jsonObj['title'] ); + + $locationsJson[] = $jsonObj; + } + + return $locationsJson; + } + + /** + * Returns the internationalized name of the mapping service. + * + * @return string + */ + public final function getName() { + return wfMessage( 'maps_' . $this->service->getName() )->text(); + } + + /** + * Returns a list of parameter information, for usage by Special:Ask and others. + * + * @return array + */ + public function getParameters() { + $params = parent::getParameters(); + $paramInfo = $this->getParameterInfo(); + + // Do not display this as an option, as the format already determines it + // TODO: this can probably be done cleaner with some changes in Maps + unset( $paramInfo['mappingservice'] ); + + $params = array_merge( $params, $paramInfo ); + + return $params; + } + + /** + * Returns an array containing the parameter info. + * + * @return array + */ + private function getParameterInfo() { + global $smgQPShowTitle, $smgQPTemplate, $smgQPHideNamespace; + + $params = array_merge( + ParamDefinition::getCleanDefinitions( MapsFunctions::getCommonParameters() ), + $this->service->getParameterInfo() + ); + + $params['staticlocations'] = [ + 'type' => 'mapslocation', + 'aliases' => [ 'locations', 'points' ], + 'default' => [], + 'islist' => true, + 'delimiter' => ';', + 'message' => 'semanticmaps-par-staticlocations', + ]; + + $params['showtitle'] = [ + 'type' => 'boolean', + 'aliases' => 'show title', + 'default' => $smgQPShowTitle, + ]; + + $params['hidenamespace'] = [ + 'type' => 'boolean', + 'aliases' => 'hide namespace', + 'default' => $smgQPHideNamespace, + ]; + + $params['template'] = [ + 'default' => $smgQPTemplate, + ]; + + $params['userparam'] = [ + 'default' => '', + ]; + + $params['activeicon'] = [ + 'type' => 'string', + 'default' => '', + ]; + + $params['pagelabel'] = [ + 'type' => 'boolean', + 'default' => false, + ]; + + $params['ajaxcoordproperty'] = [ + 'default' => '', + ]; + + $params['ajaxquery'] = [ + 'default' => '', + 'type' => 'string' + ]; + + // Messages: + // semanticmaps-par-staticlocations, semanticmaps-par-showtitle, semanticmaps-par-hidenamespace, + // semanticmaps-par-template, semanticmaps-par-userparam, semanticmaps-par-activeicon, + // semanticmaps-par-pagelabel, semanticmaps-par-ajaxcoordproperty semanticmaps-par-ajaxquery + foreach ( $params as $name => &$data ) { + if ( is_array( $data ) && !array_key_exists( 'message', $data ) ) { + $data['message'] = 'semanticmaps-par-' . $name; + } + } + + return $params; + } +} diff --git a/www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/QueryHandler.php b/www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/QueryHandler.php new file mode 100644 index 00000000..af4d2421 --- /dev/null +++ b/www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/QueryHandler.php @@ -0,0 +1,511 @@ +<?php + +namespace Maps\SemanticMW\ResultPrinters; + +use Html; +use Linker; +use Maps\Elements\Location; +use Maps\MapsFunctions; +use Maps\SemanticMW\DataValues\CoordinateValue; +use SMWDataValue; +use SMWDIGeoCoord; +use SMWPrintRequest; +use SMWQueryResult; +use SMWResultArray; +use SMWWikiPageValue; +use Title; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class QueryHandler { + + /** + * The global icon. + * @var string + */ + public $icon = ''; + + /** + * The global text. + * @var string + */ + public $text = ''; + + /** + * The global title. + * @var string + */ + public $title = ''; + + private $queryResult; + + private $outputMode; + + /** + * The template to use for the text, or false if there is none. + * @var string|boolean false + */ + private $template = false; + + /** + * Should link targets be made absolute (instead of relative)? + * @var boolean + */ + private $linkAbsolute; + + /** + * A separator to use between the subject and properties in the text field. + * @var string + */ + private $subjectSeparator = '<hr />'; + + /** + * Show the subject in the text or not? + * @var boolean + */ + private $showSubject = true; + + /** + * Hide the namespace or not. + * @var boolean + */ + private $hideNamespace = false; + + /** + * Defines which article names in the result are hyperlinked, all normally is the default + * none, subject, all + */ + private $linkStyle = 'all'; + + /* + * Show headers (with links), show headers (just text) or hide them. show is default + * show, plain, hide + */ + private $headerStyle = 'show'; + + /** + * Marker icon to show when marker equals active page + * @var string|null + */ + private $activeIcon = null; + + /** + * @var string + */ + private $userParam = ''; + + public function __construct( SMWQueryResult $queryResult, int $outputMode, bool $linkAbsolute = false ) { + $this->queryResult = $queryResult; + $this->outputMode = $outputMode; + $this->linkAbsolute = $linkAbsolute; + } + + public function setTemplate( string $template ) { + $this->template = $template === '' ? false : $template; + } + + public function setUserParam( string $userParam ) { + $this->userParam = $userParam; + } + + /** + * Sets the global icon. + */ + public function setIcon( string $icon ) { + $this->icon = $icon; + } + + /** + * Sets the global title. + */ + public function setTitle( string $title ) { + $this->title = $title; + } + + /** + * Sets the global text. + */ + public function setText( string $text ) { + $this->text = $text; + } + + public function setSubjectSeparator( string $subjectSeparator ) { + $this->subjectSeparator = $subjectSeparator; + } + + public function setShowSubject( bool $showSubject ) { + $this->showSubject = $showSubject; + } + + public function setLinkStyle( string $link ) { + $this->linkStyle = $link; + } + + public function setHeaderStyle( string $headers ) { + $this->headerStyle = $headers; + } + + /** + * @return Location[] + */ + public function getLocations(): iterable { + while ( ( $row = $this->queryResult->getNext() ) !== false ) { + yield from $this->handlePageResult( $row ); + } + } + + /** + * @param SMWResultArray[] $row + * @return Location[] + */ + private function handlePageResult( array $row ): array { + [ $title, $text ] = $this->getTitleAndText( $row[0] ); + [ $locations, $properties ] = $this->getLocationsAndProperties( $row ); + + if ( $properties !== [] && $text !== '' ) { + $text .= $this->subjectSeparator; + } + + $icon = $this->getLocationIcon( $row ); + + return $this->buildLocationsList( + $locations, + $text, + $icon, + $properties, + Title::newFromText( $title ) + ); + } + + private function getTitleAndText( SMWResultArray $resultArray ): array { + while ( ( $dataValue = $resultArray->getNextDataValue() ) !== false ) { + if ( $dataValue instanceof SMWWikiPageValue ) { + return [ + $dataValue->getLongText( $this->outputMode, null ), + $this->getResultSubjectText( $dataValue ) + ]; + } + + if ( $dataValue->getTypeID() == '_str' ) { + return [ + $dataValue->getLongText( $this->outputMode, null ), + $dataValue->getLongText( $this->outputMode, smwfGetLinker() ) + ]; + } + } + + return [ '', '' ]; + } + + /** + * @param SMWResultArray[] $row + * @return array + */ + private function getLocationsAndProperties( array $row ): array { + $locations = []; + $properties = []; + + // Loop through all fields of the record. + foreach ( $row as $i => $resultArray ) { + if ( $i === 0 ) { + continue; + } + + // Loop through all the parts of the field value. + while ( ( $dataValue = $resultArray->getNextDataValue() ) !== false ) { + if ( $dataValue instanceof \SMWRecordValue ) { + foreach ( $dataValue->getDataItems() as $dataItem ) { + if ( $dataItem instanceof \SMWDIGeoCoord ) { + $locations[] = $this->locationFromDataItem( $dataItem ); + } + } + } elseif ( $dataValue instanceof CoordinateValue ) { + $locations[] = $this->locationFromDataItem( $dataValue->getDataItem() ); + } + else { + $properties[] = $this->handleResultProperty( + $dataValue, + $resultArray->getPrintRequest() + ); + } + } + } + + return [ $locations, $properties ]; + } + + private function locationFromDataItem( SMWDIGeoCoord $dataItem ): Location { + return Location::newFromLatLon( + $dataItem->getLatitude(), + $dataItem->getLongitude() + ); + } + + /** + * Handles a SMWWikiPageValue subject value. + * Gets the plain text title and creates the HTML text with headers and the like. + * + * @param SMWWikiPageValue $object + * + * @return string + */ + private function getResultSubjectText( SMWWikiPageValue $object ): string { + if ( !$this->showSubject ) { + return ''; + } + + $dataItem = $object->getDataItem(); + + if ( $this->showArticleLink() ) { + if ( $this->linkAbsolute ) { + $text = Html::element( + 'a', + [ 'href' => $dataItem->getTitle()->getFullUrl() ], + $this->hideNamespace ? $object->getText() : $dataItem->getTitle()->getFullText() + ); + } else { + if ( $this->hideNamespace ) { + $text = $object->getShortHTMLText( smwfGetLinker() ); + } else { + $text = $object->getLongHTMLText( smwfGetLinker() ); + } + } + } else { + $text = $this->hideNamespace ? $object->getText() : $dataItem->getTitle()->getFullText(); + } + + return '<b>' . $text . '</b>'; + } + + private function showArticleLink() { + return $this->linkStyle !== 'none'; + } + + /** + * Handles a single property (SMWPrintRequest) to be displayed for a record (SMWDataValue). + */ + private function handleResultProperty( SMWDataValue $object, SMWPrintRequest $printRequest ): string { + if ( $this->hasTemplate() ) { + if ( $object instanceof SMWWikiPageValue ) { + return $object->getDataItem()->getTitle()->getPrefixedText(); + } + + return $object->getLongText( SMW_OUTPUT_WIKI, null ); + } + + $propertyName = $this->getPropertyName( $printRequest ); + return $propertyName . ( $propertyName === '' ? '' : ': ' ) . $this->getPropertyValue( $object ); + } + + private function getPropertyName( SMWPrintRequest $printRequest ): string { + if ( $this->headerStyle === 'hide' ) { + return ''; + } + + if ( $this->linkAbsolute ) { + $titleText = $printRequest->getText( null ); + $t = Title::newFromText( $titleText, SMW_NS_PROPERTY ); + + if ( $t instanceof Title && $t->exists() ) { + return Html::element( + 'a', + [ 'href' => $t->getFullUrl() ], + $printRequest->getHTMLText( null ) + ); + } + + return $titleText; + } + + return $printRequest->getHTMLText( $this->getPropertyLinker() ); + } + + private function getPropertyLinker(): ?Linker { + return $this->headerStyle === 'show' && $this->linkStyle !== 'none' ? smwfGetLinker() : null; + } + + private function getValueLinker(): ?Linker { + return $this->linkStyle === 'all' ? smwfGetLinker() : null; + } + + private function getPropertyValue( SMWDataValue $object ): string { + if ( !$this->linkAbsolute ) { + return $object->getLongHTMLText( + $this->getValueLinker() + ); + } + + if ( $this->hasPage( $object ) ) { + return Html::element( + 'a', + [ + 'href' => Title::newFromText( + $object->getLongText( $this->outputMode, null ), + NS_MAIN + )->getFullUrl() + ], + $object->getLongText( $this->outputMode, null ) + ); + } + + return $object->getLongText( $this->outputMode, null ); + } + + private function hasPage( SMWDataValue $object ): bool { + $hasPage = $object->getTypeID() == '_wpg'; + + if ( $hasPage ) { + $t = Title::newFromText( $object->getLongText( $this->outputMode, null ), NS_MAIN ); + $hasPage = $t !== null && $t->exists(); + } + + return $hasPage; + } + + private function hasTemplate() { + return is_string( $this->template ); + } + + /** + * Get the icon for a row. + * + * @param array $row + * + * @return string + */ + private function getLocationIcon( array $row ) { + $icon = ''; + $legendLabels = []; + + //Check for activeicon parameter + + if ( $this->shouldGetActiveIconUrlFor( $row[0]->getResultSubject()->getTitle() ) ) { + $icon = MapsFunctions::getFileUrl( $this->activeIcon ); + } + + // Look for display_options field, which can be set by Semantic Compound Queries + // the location of this field changed in SMW 1.5 + $display_location = method_exists( $row[0], 'getResultSubject' ) ? $row[0]->getResultSubject() : $row[0]; + + if ( property_exists( $display_location, 'display_options' ) && is_array( + $display_location->display_options + ) ) { + $display_options = $display_location->display_options; + if ( array_key_exists( 'icon', $display_options ) ) { + $icon = $display_options['icon']; + + // This is somewhat of a hack - if a legend label has been set, we're getting it for every point, instead of just once per icon + if ( array_key_exists( 'legend label', $display_options ) ) { + + $legend_label = $display_options['legend label']; + + if ( !array_key_exists( $icon, $legendLabels ) ) { + $legendLabels[$icon] = $legend_label; + } + } + } + } // Icon can be set even for regular, non-compound queries If it is, though, we have to translate the name into a URL here + elseif ( $this->icon !== '' ) { + $icon = MapsFunctions::getFileUrl( $this->icon ); + } + + return $icon; + } + + private function shouldGetActiveIconUrlFor( Title $title ) { + global $wgTitle; + + return isset( $this->activeIcon ) && is_object( $wgTitle ) + && $wgTitle->equals( $title ); + } + + /** + * Builds a set of locations with the provided title, text and icon. + * + * @param Location[] $locations + * @param string $text + * @param string $icon + * @param array $properties + * @param Title|null $title + * + * @return Location[] + */ + private function buildLocationsList( array $locations, $text, $icon, array $properties, Title $title = null ): array { + if ( !$this->hasTemplate() ) { + $text .= implode( '<br />', $properties ); + } + + $titleOutput = $this->getTitleOutput( $title ); + + foreach ( $locations as &$location ) { + if ( $this->hasTemplate() ) { + $segments = array_merge( + [ + $this->template, + 'title=' . $titleOutput, + 'latitude=' . $location->getCoordinates()->getLatitude(), + 'longitude=' . $location->getCoordinates()->getLongitude(), + 'userparam=' . $this->userParam + ], + $properties + ); + + $text .= $this->getParser()->recursiveTagParseFully( + '{{' . implode( '|', $segments ) . '}}' + ); + } + + $location->setTitle( $titleOutput ); + $location->setText( $text ); + $location->setIcon( trim( $icon ) ); + } + + return $locations; + } + + private function getTitleOutput( Title $title = null ) { + if ( $title === null ) { + return ''; + } + + return $this->hideNamespace ? $title->getText() : $title->getFullText(); + } + + /** + * @return \Parser + */ + private function getParser() { + return $GLOBALS['wgParser']; + } + + /** + * @return boolean + */ + public function getHideNamespace() { + return $this->hideNamespace; + } + + /** + * @param boolean $hideNamespace + */ + public function setHideNamespace( $hideNamespace ) { + $this->hideNamespace = $hideNamespace; + } + + /** + * @return string + */ + public function getActiveIcon() { + return $this->activeIcon; + } + + /** + * @param string $activeIcon + */ + public function setActiveIcon( $activeIcon ) { + $this->activeIcon = $activeIcon; + } + +} diff --git a/www/wiki/extensions/Maps/src/SemanticMW/ValueDescriptions/AreaDescription.php b/www/wiki/extensions/Maps/src/SemanticMW/ValueDescriptions/AreaDescription.php new file mode 100644 index 00000000..ac885095 --- /dev/null +++ b/www/wiki/extensions/Maps/src/SemanticMW/ValueDescriptions/AreaDescription.php @@ -0,0 +1,146 @@ +<?php + +namespace Maps\SemanticMW\ValueDescriptions; + +use DataValues\Geo\Values\LatLongValue; +use InvalidArgumentException; +use Maps\GeoFunctions; +use Maps\Presentation\MapsDistanceParser; +use SMW\DataValueFactory; +use SMW\DIProperty; +use SMW\Query\Language\ValueDescription; +use SMWDataItem; +use SMWDIGeoCoord; +use SMWThingDescription; +use Wikimedia\Rdbms\IDatabase; + +/** + * Description of a geographical area defined by a coordinates set and a distance to the bounds. + * The bounds are a 'rectangle' (but bend due to the earths curvature), as the resulting query + * would otherwise be to resource intensive. + * + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class AreaDescription extends ValueDescription { + + /** + * @var SMWDIGeoCoord + */ + private $center; + + private $radius; + + public function __construct( SMWDataItem $areaCenter, int $comparator, string $radius, DIProperty $property = null ) { + if ( !( $areaCenter instanceof SMWDIGeoCoord ) ) { + throw new InvalidArgumentException( '$areaCenter needs to be a SMWDIGeoCoord' ); + } + + parent::__construct( $areaCenter, $property, $comparator ); + + $this->center = $areaCenter; + $this->radius = $radius; + } + + /** + * @see Description::prune + */ + public function prune( &$maxsize, &$maxdepth, &$log ) { + if ( ( $maxsize < $this->getSize() ) || ( $maxdepth < $this->getDepth() ) ) { + $log[] = $this->getQueryString(); + + $result = new SMWThingDescription(); + $result->setPrintRequests( $this->getPrintRequests() ); + + return $result; + } + + $maxsize = $maxsize - $this->getSize(); + $maxdepth = $maxdepth - $this->getDepth(); + + return $this; + } + + public function getQueryString( $asValue = false ) { + $centerString = DataValueFactory::getInstance()->newDataValueByItem( + $this->center, + $this->getProperty() + )->getWikiValue(); + + $queryString = "$centerString ({$this->radius})"; + + return $asValue ? $queryString : "[[$queryString]]"; + } + + /** + * @see SomePropertyInterpreter::mapValueDescription + * + * FIXME: store specific code should be in the store component + * + * @param string $tableName + * @param string[] $fieldNames + * @param IDatabase $db + * + * @return string|false + */ + public function getSQLCondition( $tableName, array $fieldNames, IDatabase $db ) { + if ( $this->center->getDIType() != SMWDataItem::TYPE_GEO ) { + throw new \LogicException( 'Constructor should have prevented this' ); + } + + if ( !$this->comparatorIsSupported() ) { + return false; + } + + $bounds = $this->getBoundingBox(); + + $north = $db->addQuotes( $bounds['north'] ); + $east = $db->addQuotes( $bounds['east'] ); + $south = $db->addQuotes( $bounds['south'] ); + $west = $db->addQuotes( $bounds['west'] ); + + $isEq = $this->getComparator() == SMW_CMP_EQ; + + $smallerThen = $isEq ? '<' : '>='; + $biggerThen = $isEq ? '>' : '<='; + $joinCond = $isEq ? 'AND' : 'OR'; + + $conditions = []; + + $conditions[] = "{$tableName}.$fieldNames[1] $smallerThen $north"; + $conditions[] = "{$tableName}.$fieldNames[1] $biggerThen $south"; + $conditions[] = "{$tableName}.$fieldNames[2] $smallerThen $east"; + $conditions[] = "{$tableName}.$fieldNames[2] $biggerThen $west"; + + return implode( " $joinCond ", $conditions ); + } + + private function comparatorIsSupported(): bool { + return $this->getComparator() === SMW_CMP_EQ || $this->getComparator() === SMW_CMP_NEQ; + } + + /** + * @return float[] An associative array containing the limits with keys north, east, south and west. + */ + public function getBoundingBox(): array { + $center = new LatLongValue( + $this->center->getLatitude(), + $this->center->getLongitude() + ); + + $radiusInMeters = MapsDistanceParser::parseDistance( $this->radius ); // TODO: this can return false + + $north = GeoFunctions::findDestination( $center, 0, $radiusInMeters ); + $east = GeoFunctions::findDestination( $center, 90, $radiusInMeters ); + $south = GeoFunctions::findDestination( $center, 180, $radiusInMeters ); + $west = GeoFunctions::findDestination( $center, 270, $radiusInMeters ); + + return [ + 'north' => $north['lat'], + 'east' => $east['lon'], + 'south' => $south['lat'], + 'west' => $west['lon'], + ]; + } + +} diff --git a/www/wiki/extensions/Maps/src/SemanticMW/ValueDescriptions/CoordinateDescription.php b/www/wiki/extensions/Maps/src/SemanticMW/ValueDescriptions/CoordinateDescription.php new file mode 100644 index 00000000..a63ea9a0 --- /dev/null +++ b/www/wiki/extensions/Maps/src/SemanticMW/ValueDescriptions/CoordinateDescription.php @@ -0,0 +1,74 @@ +<?php + +namespace Maps\SemanticMW\ValueDescriptions; + +use SMW\DataValueFactory; +use SMW\Query\Language\ValueDescription; +use SMWDIGeoCoord; +use Wikimedia\Rdbms\IDatabase; + +/** + * Description of one data value of type Geographical Coordinates. + * + * @author Jeroen De Dauw + */ +class CoordinateDescription extends ValueDescription { + + public function getQueryString( $asValue = false ) { + $queryString = DataValueFactory::getInstance()->newDataValueByItem( + $this->getDataItem(), + $this->getProperty() + )->getWikiValue(); + + return $asValue ? $queryString : "[[$queryString]]"; + } + + /** + * @see SomePropertyInterpreter::mapValueDescription + * + * FIXME: store specific code should be in the store component + * + * @param string $tableName + * @param string[] $fieldNames + * @param IDatabase $db + * + * @return string|false + */ + public function getSQLCondition( $tableName, array $fieldNames, IDatabase $db ) { + $dataItem = $this->getDataItem(); + + // Only execute the query when the description's type is geographical coordinates, + // the description is valid, and the near comparator is used. + if ( $dataItem instanceof SMWDIGeoCoord ) { + switch ( $this->getComparator() ) { + case SMW_CMP_EQ: + $comparator = '='; + break; + case SMW_CMP_LEQ: + $comparator = '<='; + break; + case SMW_CMP_GEQ: + $comparator = '>='; + break; + case SMW_CMP_NEQ: + $comparator = '!='; + break; + default: + return false; + } + + $lat = $db->addQuotes( $dataItem->getLatitude() ); + $lon = $db->addQuotes( $dataItem->getLongitude() ); + + $conditions = []; + + $conditions[] = "{$tableName}.$fieldNames[1] $comparator $lat"; + $conditions[] = "{$tableName}.$fieldNames[2] $comparator $lon"; + + return implode( ' AND ', $conditions ); + } + + return false; + } + +} diff --git a/www/wiki/extensions/Maps/src/SemanticMaps.php b/www/wiki/extensions/Maps/src/SemanticMaps.php new file mode 100644 index 00000000..8d48dc56 --- /dev/null +++ b/www/wiki/extensions/Maps/src/SemanticMaps.php @@ -0,0 +1,74 @@ +<?php + +namespace Maps; + +use Maps\SemanticMW\ResultPrinters\KmlPrinter; +use Maps\SemanticMW\ResultPrinters\MapPrinter; + +/** + * @licence GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +class SemanticMaps { + + private $mwGlobals; + + private function __construct( array &$mwGlobals ) { + $this->mwGlobals =& $mwGlobals; + } + + public static function newFromMediaWikiGlobals( array &$mwGlobals ) { + return new self( $mwGlobals ); + } + + public function initExtension() { + // Hook for initializing the Geographical Data types. + $this->mwGlobals['wgHooks']['SMW::DataType::initTypes'][] = 'Maps\MediaWiki\SemanticMapsHooks::initGeoDataTypes'; + + // Hook for defining the default query printer for queries that ask for geographical coordinates. + $this->mwGlobals['wgHooks']['SMWResultFormat'][] = 'Maps\MediaWiki\SemanticMapsHooks::addGeoCoordsDefaultFormat'; + + // Hook for adding a Semantic Maps links to the Admin Links extension. + $this->mwGlobals['wgHooks']['AdminLinks'][] = 'Maps\MediaWiki\SemanticMapsHooks::addToAdminLinks'; + + $this->registerGoogleMaps(); + $this->registerLeaflet(); + + $this->mwGlobals['smwgResultFormats']['kml'] = KmlPrinter::class; + + $this->mwGlobals['smwgResultAliases'][$this->mwGlobals['egMapsDefaultService']][] = 'map'; + MapPrinter::registerDefaultService( $this->mwGlobals['egMapsDefaultService'] ); + + // Internationalization + $this->mwGlobals['wgMessagesDirs']['SemanticMaps'] = __DIR__ . '/i18n'; + } + + private function registerGoogleMaps() { + // TODO: inject + $services = MapsFactory::globalInstance()->getMappingServices(); + + if ( $services->nameIsKnown( 'googlemaps3' ) ) { + $googleMaps = $services->getService( 'googlemaps3' ); + + MapPrinter::registerService( $googleMaps ); + + $this->mwGlobals['smwgResultFormats'][$googleMaps->getName()] = MapPrinter::class; + $this->mwGlobals['smwgResultAliases'][$googleMaps->getName()] = $googleMaps->getAliases(); + } + } + + private function registerLeaflet() { + // TODO: inject + $services = MapsFactory::globalInstance()->getMappingServices(); + + if ( $services->nameIsKnown( 'leaflet' ) ) { + $leaflet = $services->getService( 'leaflet' ); + + MapPrinter::registerService( $leaflet ); + + $this->mwGlobals['smwgResultFormats'][$leaflet->getName()] = MapPrinter::class; + $this->mwGlobals['smwgResultAliases'][$leaflet->getName()] = $leaflet->getAliases(); + } + } + +} |