summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Maps/src
diff options
context:
space:
mode:
authorYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
committerYaco <franco@reevo.org>2020-06-04 11:01:00 -0300
commitfc7369835258467bf97eb64f184b93691f9a9fd5 (patch)
treedaabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/extensions/Maps/src
first commit
Diffstat (limited to 'www/wiki/extensions/Maps/src')
-rw-r--r--www/wiki/extensions/Maps/src/DataAccess/CachingGeocoder.php46
-rw-r--r--www/wiki/extensions/Maps/src/DataAccess/JsonFileParser.php79
-rw-r--r--www/wiki/extensions/Maps/src/DataAccess/MapsFileFetcher.php26
-rw-r--r--www/wiki/extensions/Maps/src/DataAccess/MediaWikiFileUrlFinder.php31
-rw-r--r--www/wiki/extensions/Maps/src/DataAccess/PageContentFetcher.php40
-rw-r--r--www/wiki/extensions/Maps/src/Elements/BaseElement.php57
-rw-r--r--www/wiki/extensions/Maps/src/Elements/BaseFillableElement.php45
-rw-r--r--www/wiki/extensions/Maps/src/Elements/BaseStrokableElement.php63
-rw-r--r--www/wiki/extensions/Maps/src/Elements/Circle.php62
-rw-r--r--www/wiki/extensions/Maps/src/Elements/ImageOverlay.php36
-rw-r--r--www/wiki/extensions/Maps/src/Elements/Line.php69
-rw-r--r--www/wiki/extensions/Maps/src/Elements/Location.php157
-rw-r--r--www/wiki/extensions/Maps/src/Elements/Polygon.php44
-rw-r--r--www/wiki/extensions/Maps/src/Elements/Rectangle.php80
-rw-r--r--www/wiki/extensions/Maps/src/Elements/WmsOverlay.php71
-rw-r--r--www/wiki/extensions/Maps/src/FileUrlFinder.php18
-rw-r--r--www/wiki/extensions/Maps/src/GeoFunctions.php99
-rw-r--r--www/wiki/extensions/Maps/src/GoogleMapsService.php314
-rw-r--r--www/wiki/extensions/Maps/src/LeafletService.php184
-rw-r--r--www/wiki/extensions/Maps/src/MappingService.php33
-rw-r--r--www/wiki/extensions/Maps/src/MappingServices.php82
-rw-r--r--www/wiki/extensions/Maps/src/MapsFactory.php168
-rw-r--r--www/wiki/extensions/Maps/src/MapsFunctions.php218
-rw-r--r--www/wiki/extensions/Maps/src/MapsSetup.php206
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/Content/GeoJsonContent.php49
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/Content/GeoJsonContentHandler.php15
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/MapsHooks.php68
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/CoordinatesFunction.php102
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DisplayMapFunction.php171
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DisplayMapRenderer.php182
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/DistanceFunction.php100
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/FindDestinationFunction.php120
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/GeoDistanceFunction.php117
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/GeocodeFunction.php113
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/ParserHooks/MapsDocFunction.php200
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/SemanticMapsHooks.php115
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/Specials/MapEditorHTML.php221
-rw-r--r--www/wiki/extensions/Maps/src/MediaWiki/Specials/SpecialMapEditor.php69
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/CoordinateFormatter.php36
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/ElementJsonSerializer.php35
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/KmlFormatter.php78
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/MapHtmlBuilder.php37
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/MapsDistanceParser.php133
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/ParameterExtractor.php47
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/WikitextParser.php30
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/WikitextParsers/CircleParser.php86
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/WikitextParsers/DistanceParser.php38
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/WikitextParsers/ImageOverlayParser.php83
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/WikitextParsers/LineParser.php163
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/WikitextParsers/LocationParser.php147
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/WikitextParsers/PolygonParser.php41
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/WikitextParsers/RectangleParser.php89
-rw-r--r--www/wiki/extensions/Maps/src/Presentation/WikitextParsers/WmsOverlayParser.php49
-rw-r--r--www/wiki/extensions/Maps/src/SemanticMW/DataValues/CoordinateValue.php266
-rw-r--r--www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/KmlPrinter.php146
-rw-r--r--www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/MapPrinter.php403
-rw-r--r--www/wiki/extensions/Maps/src/SemanticMW/ResultPrinters/QueryHandler.php511
-rw-r--r--www/wiki/extensions/Maps/src/SemanticMW/ValueDescriptions/AreaDescription.php146
-rw-r--r--www/wiki/extensions/Maps/src/SemanticMW/ValueDescriptions/CoordinateDescription.php74
-rw-r--r--www/wiki/extensions/Maps/src/SemanticMaps.php74
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();
+ }
+ }
+
+}