diff --git a/assets/controllers/map_controller.js b/assets/controllers/map_controller.js index 4ca17f1..ba12148 100644 --- a/assets/controllers/map_controller.js +++ b/assets/controllers/map_controller.js @@ -133,7 +133,15 @@ export default class extends Controller { case 'warning' : color = '#ffc107'; break case 'success' : color = '#198754'; break } - return {color: color}; + if (feature.geometry.type === 'Polygon') { + return { + color: color, + weight: 1, + fillOpacity: 0.5, + }; + } else { + return {color: color}; + } } }).bindTooltip(feature.properties.name).addTo(taskLayer).on('click', function (event) { window.location.href = event.layer.feature.properties.url; diff --git a/composer.json b/composer.json index b8fa49a..7eb7794 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.2", "jbelien/oauth2-openstreetmap": "^0.1.2", - "jmikola/geojson": "^1.0", + "jmikola/geojson": "^1.2", "knplabs/knp-paginator-bundle": "^6.4", "knpuniversity/oauth2-client-bundle": "^2.18", "league/commonmark": "^2.4", @@ -58,7 +58,8 @@ }, "autoload": { "psr-4": { - "App\\": "src/" + "App\\": "src/", + "OSM\\": "lib/OSM/" } }, "autoload-dev": { diff --git a/composer.lock b/composer.lock index 1673882..19a4f92 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "849dadca70e58b40b2c1cc3989eb53ac", + "content-hash": "b5bbf254d893ea1ef44505e824cdf2b9", "packages": [ { "name": "behat/transliterator", diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index b9d1ac3..085abac 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -5,12 +5,20 @@ twig: title: '%short_title%' menu: header: - - { route: 'app_project', label: 'Projets', icon: '' } + - route: 'app_project' + label: 'Projets' + icon: '' + - route: 'app_tools' + label: 'Outils' + icon: '' taskLifecycleManager: '@App\Service\TaskLifecycleManager' geoJsonManager: '@App\Service\GeoJsonManager' sourceGenerator: '@App\Service\SourceGenerator' short_title: '%short_title%' long_title: '%long_title%' + tools: + - label: 'Communes' + route: 'app_tools_city' when@test: twig: diff --git a/lib/OSM/Box.php b/lib/OSM/Box.php new file mode 100644 index 0000000..c40e5b6 --- /dev/null +++ b/lib/OSM/Box.php @@ -0,0 +1,12 @@ +id = (int) $array['id']; + + $hasMembers = isset($array['members']); + if ($hasMembers) { + $items = $array['members']; + foreach ($items as $item) { + $member = Member::createFromArray($item); + $instance->members[] = $member; + } + } + + $hasTags = isset($array['tags']); + if ($hasTags) { + $items = $array['tags']; + foreach ($items as $itemKey => $itemValue) { + $tag = Tag::createFromValues($itemKey, $itemValue); + $instance->tags[] = $tag; + } + } + + $instance->completeFromArray($array); + + return $instance; + } + + public function completeFromArray(array $array): static + { + return $this; + } + + public function getTagValue(string $key): ?string { + $foundTags = array_filter($this->tags, function ($tag) use ($key) { + return $tag->key === $key; + }); + + if (count($foundTags) !== 1) { + return null; + } + + $tag = reset($foundTags); + return $tag->value; + } +} diff --git a/lib/OSM/Element/Member/Member.php b/lib/OSM/Element/Member/Member.php new file mode 100644 index 0000000..6e8cf22 --- /dev/null +++ b/lib/OSM/Element/Member/Member.php @@ -0,0 +1,38 @@ +ref = (int) $array['ref']; + + $hasRole = isset($array['role']); + assert($hasRole); + $instance->role = (string) $array['role']; + + $instance->completeFromArray($array); + + return $instance; + } + + public function isSame(Member $other): bool { + return ($this->ref === $other->ref); + } + +} diff --git a/lib/OSM/Element/Member/Node.php b/lib/OSM/Element/Member/Node.php new file mode 100644 index 0000000..daf3801 --- /dev/null +++ b/lib/OSM/Element/Member/Node.php @@ -0,0 +1,19 @@ +point = Point::createFromArray($array); + + return $this; + } + +} diff --git a/lib/OSM/Element/Member/Way.php b/lib/OSM/Element/Member/Way.php new file mode 100644 index 0000000..1f2ea19 --- /dev/null +++ b/lib/OSM/Element/Member/Way.php @@ -0,0 +1,38 @@ +points[] = $point; + } + } + + return $this; + } + + public function getFirstPoint(): Point { + return reset($this->points); + } + + public function getLastPoint(): Point { + return end($this->points); + } + + public function reversePoints(): static { + $this->points = array_reverse($this->points); + return $this; + } +} diff --git a/lib/OSM/Element/Relation.php b/lib/OSM/Element/Relation.php new file mode 100644 index 0000000..b9dfba0 --- /dev/null +++ b/lib/OSM/Element/Relation.php @@ -0,0 +1,86 @@ +members, function ($member) { + return ( + ($member instanceof \OSM\Element\Member\Way) + and ($member->role === 'outer') + ); + }); + } + + public function getOuterWaysStartingWith(Point $point, ?Way $exclude = null): array { + return array_filter($this->members, function ($member) use ($point, $exclude) { + return ( + ($member instanceof \OSM\Element\Member\Way) + and ($member->role === 'outer') + and ($point->isSame($member->getFirstPoint())) + and ( + is_null($exclude) + or !$member->isSame($exclude) + ) + ); + }); + } + + public function getOuterWaysStopingWith(Point $point, ?Way $exclude = null): array { + return array_filter($this->members, function ($member) use ($point, $exclude) { + return ( + ($member instanceof \OSM\Element\Member\Way) + and ($member->role === 'outer') + and ($point->isSame($member->getLastPoint())) + and ( + is_null($exclude) + or !$member->isSame($exclude) + ) + ); + }); + } + + public function getOrderedOuterWays(): array { + $orderedWays = []; + $ways = $this->getOuterWays(); + + $currentWay = reset($ways); + $veryFirstPoint = $currentWay->getFirstPoint(); + $isDone = false; + while (!$isDone) { + $orderedWays[] = $currentWay; + $nextWays = $this->getOuterWaysStartingWith($currentWay->getLastPoint(), $currentWay); + assert(count($nextWays) <= 1); + if (count($nextWays) === 1) { + $nextWay = reset($nextWays); + if ($veryFirstPoint->isSame($nextWay->getFirstPoint())) { + break; + } + $currentWay = $nextWay; + continue; + } else { + $nextWays = $this->getOuterWaysStopingWith($currentWay->getLastPoint(), $currentWay); + assert(count($nextWays) <= 1); + if (count($nextWays) === 1) { + $nextWay = reset($nextWays); + $nextWay->reversePoints(); + if ($veryFirstPoint->isSame($nextWay->getFirstPoint())) { + break; + } + $currentWay = $nextWay; + continue; + } else { + $isDone = true; + } + } + } + + return $orderedWays; + } + +} diff --git a/lib/OSM/Element/Tag.php b/lib/OSM/Element/Tag.php new file mode 100644 index 0000000..e35376d --- /dev/null +++ b/lib/OSM/Element/Tag.php @@ -0,0 +1,22 @@ +key = $key; + $instance->value = $value; + + return $instance; + } +} diff --git a/lib/OSM/GeoJsonConverter.php b/lib/OSM/GeoJsonConverter.php new file mode 100644 index 0000000..1df9b2a --- /dev/null +++ b/lib/OSM/GeoJsonConverter.php @@ -0,0 +1,25 @@ +getOrderedOuterWays() as $way) { + foreach ($way->points as $point) { + $positions[] = new \GeoJson\Geometry\Point([ + $point->longitude, + $point->latitude + ]); + } + } + + return new \GeoJson\Geometry\Polygon([ $positions ]); + } + +} diff --git a/lib/OSM/OSM.php b/lib/OSM/OSM.php new file mode 100644 index 0000000..e6e5710 --- /dev/null +++ b/lib/OSM/OSM.php @@ -0,0 +1,25 @@ +elements[] = $element; + } + + return $instance; + } + +} diff --git a/lib/OSM/Point.php b/lib/OSM/Point.php new file mode 100644 index 0000000..aaf8d94 --- /dev/null +++ b/lib/OSM/Point.php @@ -0,0 +1,30 @@ +latitude = (float) $array['lat']; + $instance->longitude = (float) $array['lon']; + + return $instance; + } + + public function isSame(Point $other): bool { + $isSame = ( + ($other->latitude === $this->latitude) + and ($other->longitude === $this->longitude) + ); + return $isSame; + } +} diff --git a/php.ini b/php.ini new file mode 100644 index 0000000..e7b8ad0 --- /dev/null +++ b/php.ini @@ -0,0 +1,4 @@ +upload_max_filesize = 40M +post_max_size = 40M +memory_limit = 256M + diff --git a/src/Controller/TaskController.php b/src/Controller/TaskController.php index faf9cb6..ea0db01 100644 --- a/src/Controller/TaskController.php +++ b/src/Controller/TaskController.php @@ -112,9 +112,11 @@ class TaskController extends AbstractController 'imagery' => [ 'id' => $project->hasImagery() ? $project->getImagery() : 'osmfr', ], + ]; + + if ($task->hasOsm() and !empty($task->getOsm())) { // Charge le XML OSM du projet danms un calque de données - // TODO C’est susceptible de planter s’il n’y a pas d’OSM dans le projet mais est-ce possible ? - 'import' => [ + $josmCommands['import'] = [ 'new_layer' => true, 'layer_name' => $task->getName(), 'url' => $this->generateUrl( @@ -122,8 +124,8 @@ class TaskController extends AbstractController ['slug' => $task->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL ), - ], - ]; + ]; + } if ($task->hasGeojson()) { $geom = \geoPHP::load($task->getGeojson(), 'json'); diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php new file mode 100644 index 0000000..6d4e761 --- /dev/null +++ b/src/Controller/ToolsController.php @@ -0,0 +1,96 @@ +render('tools/index.html.twig', [ + ]); + } + + #[Route('/city', name: 'app_tools_city')] + public function city( + Request $request, + OverpassClient $overpass, + ): Response + { + $form = $this->createForm(CityToolType::class, []); + $form->add('submit', SubmitType::class, ['label' => 'Générer']); + + $form->handleRequest($request); + if ($form->isSubmitted() and $form->isValid()) { + $areaId = $form->get('area')->getData(); + $query = sprintf('relation["boundary"="administrative"]["admin_level"="8"]["name"](area:%d);', 3600000000 + $areaId); // Cf. + $json = $overpass->query($query); + + $response = new StreamedResponse(); + + $response->headers->set('Content-Type', 'text/csv'); + $response->headers->set( + 'Content-Disposition', + HeaderUtils::makeDisposition( + HeaderUtils::DISPOSITION_ATTACHMENT, + sprintf('cities-in-%d.csv', $areaId) + ) + ); + + $response->setCallback(function () use ($json): void { + $headings = [ + 'name', + 'description', + 'osm', + 'geojson', + 'status', + ]; + + $csv = fopen('php://output', 'a'); + + fputcsv($csv, $headings); + + $osm = \OSM\OSM::createFromJson($json); + + foreach ($osm->elements as $relation) { + $name = $relation->getTagValue('name'); + + $feature = new \GeoJson\Feature\Feature( + \OSM\GeoJsonConverter::convertRelationToPolygon($relation), + ['name' => $name] + ); + + fputcsv($csv, [ + $name, + $name, + '', + json_encode(new \GeoJson\Feature\FeatureCollection([ $feature ])), + 'todo', + ]); + } + + fclose($csv); + }); + return $response; + } + + return $this->render('tools/city.html.twig', [ + 'form' => $form, + ]); + } + +} diff --git a/src/Entity/Task.php b/src/Entity/Task.php index c7f6508..0908be4 100644 --- a/src/Entity/Task.php +++ b/src/Entity/Task.php @@ -181,6 +181,11 @@ class Task return $this; } + public function hasOsm(): bool + { + return isset($this->osm); + } + public function getOsm(): ?string { return $this->osm; diff --git a/src/Form/CityToolType.php b/src/Form/CityToolType.php new file mode 100644 index 0000000..7cac502 --- /dev/null +++ b/src/Form/CityToolType.php @@ -0,0 +1,56 @@ +requestStack->getSession(); + $choices = $session->has('city_tool_choices') ? $session->get('city_tool_choices') : []; + + if (empty($choices)) { + $entries = $this->overpass->rawQuery('[out:csv(::id,"ref","name")][timeout:25];area(3602202162)->.searchArea;relation["name"]["boundary"="administrative"]["border_type"="departement"]["admin_level"="6"](area.searchArea);out;'); + $col = []; + $row = []; + $entries = explode("\n", $entries); + foreach ($entries as $index => $entry) { + $entry = explode("\t", $entry); + if ($index === 0) { + $col = $entry; + } else { + if (count($col) === count($entry)) { + $row = array_combine($col, $entry); + $key = sprintf('%s %s', $row['ref'], $row['name']); + $val = (int) $row['@id']; + $choices[$key] = $val; + } + } + } + ksort($choices); + } + + $session->set('city_tool_choices', $choices); + + $builder + ->add('area', ChoiceType::class, [ + 'label' => 'Département', + 'choices' => $choices, + ]) + ; + } +} diff --git a/src/Service/OverpassClient.php b/src/Service/OverpassClient.php index 3c795b3..d928048 100644 --- a/src/Service/OverpassClient.php +++ b/src/Service/OverpassClient.php @@ -29,4 +29,20 @@ class OverpassClient return $response->getContent(); } + public function rawQuery($query) + { + $response = $this->client->request('GET', 'https://overpass-api.de/api/interpreter', [ + 'query' => [ + 'data' => $query, + ], + ]); + + $isStatusCodeOk = (200 === $response->getStatusCode()); + + if (!$isStatusCodeOk) { + throw new \RuntimeException(); + } + + return $response->getContent(); + } } diff --git a/templates/project/show.html.twig b/templates/project/show.html.twig index 3886b28..3577234 100644 --- a/templates/project/show.html.twig +++ b/templates/project/show.html.twig @@ -96,12 +96,14 @@

{% include 'partials/_project-metadata.html.twig' %}
+ {% if project.hashtags is not empty %} {% for hashtag in project.hashtags|split(' ') %} {{ '#' ~ hashtag }} {% endfor %} + {% endif %}

@@ -161,6 +163,7 @@ +{{ knp_pagination_render(tasks) }} {% endif %} {% if comments is not empty %} diff --git a/templates/tools/base.html.twig b/templates/tools/base.html.twig new file mode 100644 index 0000000..60ebc40 --- /dev/null +++ b/templates/tools/base.html.twig @@ -0,0 +1,14 @@ +{% extends 'base.html.twig' %} + +{% block page_title %}Outils{% endblock %} + +{% block page_content %} + +{% block tools_content %}{% endblock %} +{% endblock %} diff --git a/templates/tools/city.html.twig b/templates/tools/city.html.twig new file mode 100644 index 0000000..c5df176 --- /dev/null +++ b/templates/tools/city.html.twig @@ -0,0 +1,5 @@ +{% extends 'tools/base.html.twig' %} + +{% block tools_content %} +{{ form(form) }} +{% endblock %} diff --git a/templates/tools/index.html.twig b/templates/tools/index.html.twig new file mode 100644 index 0000000..bebcb7f --- /dev/null +++ b/templates/tools/index.html.twig @@ -0,0 +1,2 @@ +{% extends 'tools/base.html.twig' %} +