| @ -0,0 +1,12 @@ | |||
| <?php | |||
| namespace OSM; | |||
| use OSM\Point; | |||
| class Box { | |||
| public Point $mininum; | |||
| public Point $maximum; | |||
| } | |||
| @ -0,0 +1,70 @@ | |||
| <?php | |||
| namespace OSM\Element; | |||
| use OSM\Element\Member\Member; | |||
| use OSM\Element\Tag; | |||
| class Element { | |||
| public ?int $id = null; | |||
| public array $members = []; | |||
| public array $tags = []; | |||
| public static function createFromArray($array) { | |||
| assert(is_array($array)); | |||
| $hasType = isset($array['type']); | |||
| assert($hasType); | |||
| $className = __NAMESPACE__.'\\'.ucfirst($array['type']); | |||
| $hasClass = class_exists($className); | |||
| $instance = new $className(); | |||
| $hasId = isset($array['id']); | |||
| assert($hasId); | |||
| $instance->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; | |||
| } | |||
| } | |||
| @ -0,0 +1,38 @@ | |||
| <?php | |||
| namespace OSM\Element\Member; | |||
| class Member { | |||
| public int $ref; | |||
| public string $role; | |||
| public static function createFromArray($array) { | |||
| assert(is_array($array)); | |||
| $hasType = isset($array['type']); | |||
| assert($hasType); | |||
| $className = __NAMESPACE__.'\\'.ucfirst($array['type']); | |||
| $hasClass = class_exists($className); | |||
| $instance = new $className(); | |||
| $hasRef = isset($array['ref']); | |||
| assert($hasRef); | |||
| $instance->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); | |||
| } | |||
| } | |||
| @ -0,0 +1,19 @@ | |||
| <?php | |||
| namespace OSM\Element\Member; | |||
| use OSM\Element\Member\Member; | |||
| use OSM\Point; | |||
| class Node extends Member { | |||
| public Point $point; | |||
| public function completeFromArray(array $array): static | |||
| { | |||
| $this->point = Point::createFromArray($array); | |||
| return $this; | |||
| } | |||
| } | |||
| @ -0,0 +1,38 @@ | |||
| <?php | |||
| namespace OSM\Element\Member; | |||
| use OSM\Element\Member\Member; | |||
| use OSM\Point; | |||
| class Way extends Member { | |||
| public array $points = []; | |||
| public function completeFromArray(array $array): static | |||
| { | |||
| $hasGeometry = isset($array['geometry']); | |||
| if ($hasGeometry) { | |||
| $items = $array['geometry']; | |||
| foreach ($items as $item) { | |||
| $point = Point::createFromArray($item); | |||
| $this->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; | |||
| } | |||
| } | |||
| @ -0,0 +1,86 @@ | |||
| <?php | |||
| namespace OSM\Element; | |||
| use OSM\Element\Element; | |||
| use OSM\Element\Member\Way; | |||
| use OSM\Point; | |||
| class Relation extends Element { | |||
| public function getOuterWays(): array { | |||
| return array_filter($this->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; | |||
| } | |||
| } | |||
| @ -0,0 +1,22 @@ | |||
| <?php | |||
| namespace OSM\Element; | |||
| class Tag { | |||
| public ?string $key; | |||
| public ?string $value; | |||
| public static function createFromValues( | |||
| string $key, | |||
| string $value | |||
| ): static | |||
| { | |||
| $instance = new self(); | |||
| $instance->key = $key; | |||
| $instance->value = $value; | |||
| return $instance; | |||
| } | |||
| } | |||
| @ -0,0 +1,25 @@ | |||
| <?php | |||
| namespace OSM; | |||
| class GeoJsonConverter { | |||
| public static function convertRelationToPolygon( | |||
| \OSM\Element\Relation $relation | |||
| ): \GeoJson\Geometry\Polygon | |||
| { | |||
| $positions = []; | |||
| foreach ($relation->getOrderedOuterWays() as $way) { | |||
| foreach ($way->points as $point) { | |||
| $positions[] = new \GeoJson\Geometry\Point([ | |||
| $point->longitude, | |||
| $point->latitude | |||
| ]); | |||
| } | |||
| } | |||
| return new \GeoJson\Geometry\Polygon([ $positions ]); | |||
| } | |||
| } | |||
| @ -0,0 +1,25 @@ | |||
| <?php | |||
| namespace OSM; | |||
| use OSM\Element\Element; | |||
| class OSM { | |||
| public array $elements = []; | |||
| public static function createFromJson($json) { | |||
| $array = json_decode($json, true); | |||
| $instance = new self(); | |||
| $items = $array['elements']; | |||
| foreach ($items as $item) { | |||
| $element = Element::createFromArray($item); | |||
| $instance->elements[] = $element; | |||
| } | |||
| return $instance; | |||
| } | |||
| } | |||
| @ -0,0 +1,30 @@ | |||
| <?php | |||
| namespace OSM; | |||
| class Point { | |||
| public float $latitude; | |||
| public float $longitude; | |||
| public static function createFromArray(array $array) | |||
| { | |||
| $hasLat = isset($array['lat']); | |||
| $hasLon = isset($array['lon']); | |||
| assert($hasLat and $hasLon); | |||
| $instance = new self(); | |||
| $instance->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; | |||
| } | |||
| } | |||
| @ -0,0 +1,4 @@ | |||
| upload_max_filesize = 40M | |||
| post_max_size = 40M | |||
| memory_limit = 256M | |||
| @ -0,0 +1,96 @@ | |||
| <?php | |||
| namespace App\Controller; | |||
| use App\Form\CityToolType; | |||
| use App\Service\OverpassClient; | |||
| use KnpU\OAuth2ClientBundle\Client\ClientRegistry; | |||
| use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; | |||
| use Symfony\Bundle\SecurityBundle\Security; | |||
| use Symfony\Component\Form\Extension\Core\Type\SubmitType; | |||
| use Symfony\Component\HttpFoundation\HeaderUtils; | |||
| use Symfony\Component\HttpFoundation\Request; | |||
| use Symfony\Component\HttpFoundation\Response; | |||
| use Symfony\Component\HttpFoundation\ResponseHeaderBag; | |||
| use Symfony\Component\HttpFoundation\StreamedResponse; | |||
| use Symfony\Component\Routing\Attribute\Route; | |||
| #[Route('/tools')] | |||
| class ToolsController extends AbstractController | |||
| { | |||
| #[Route('/', name: 'app_tools')] | |||
| public function index(): Response | |||
| { | |||
| return $this->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. <https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#By_area_.28area.29> | |||
| $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, | |||
| ]); | |||
| } | |||
| } | |||
| @ -0,0 +1,56 @@ | |||
| <?php | |||
| namespace App\Form; | |||
| use App\Service\OverpassClient; | |||
| use Symfony\Component\Form\AbstractType; | |||
| use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | |||
| use Symfony\Component\Form\Extension\Core\Type\NumberType; | |||
| use Symfony\Component\Form\FormBuilderInterface; | |||
| use Symfony\Component\HttpFoundation\RequestStack; | |||
| use Symfony\Component\Validator\Constraints\File; | |||
| class CityToolType extends AbstractType | |||
| { | |||
| public function __construct( | |||
| private OverpassClient $overpass, | |||
| private RequestStack $requestStack, | |||
| ) { | |||
| } | |||
| public function buildForm(FormBuilderInterface $builder, array $options): void | |||
| { | |||
| $session = $this->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, | |||
| ]) | |||
| ; | |||
| } | |||
| } | |||
| @ -0,0 +1,14 @@ | |||
| {% extends 'base.html.twig' %} | |||
| {% block page_title %}Outils{% endblock %} | |||
| {% block page_content %} | |||
| <ul class="nav nav-tabs mb-3"> | |||
| {% for tool in tools %} | |||
| <li class="nav-item"> | |||
| <a class="nav-link{% if app.current_route == tool.route %} active" aria-current="page{% endif %}" href="{{ path(tool.route) }}">{{ tool.label }}</a> | |||
| </li> | |||
| {% endfor %} | |||
| </ul> | |||
| {% block tools_content %}{% endblock %} | |||
| {% endblock %} | |||
| @ -0,0 +1,5 @@ | |||
| {% extends 'tools/base.html.twig' %} | |||
| {% block tools_content %} | |||
| {{ form(form) }} | |||
| {% endblock %} | |||
| @ -0,0 +1,2 @@ | |||
| {% extends 'tools/base.html.twig' %} | |||