recup sources

This commit is contained in:
Tykayn 2025-09-01 18:28:23 +02:00 committed by tykayn
parent 86622a19ea
commit 65fe2a35f9
155 changed files with 50969 additions and 0 deletions

47
.env Normal file
View file

@ -0,0 +1,47 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=abcd13223131_CHANGE_THIS_BEARER_TOKEN
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
###> symfony/mailer ###
MAILER_DSN=null://null
###< symfony/mailer ###
#DEBUG=0
APP_OSM_BEARER=CHANGE_IT
MAPBOX_TOKEN=
MAPTILER_TOKEN=
USE_PLACES_WITHOUT_EMAIL_TO_REFERENCE=false

0
.env.dev Normal file
View file

6
.env.test Normal file
View file

@ -0,0 +1,6 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots

33
.gitignore vendored Normal file
View file

@ -0,0 +1,33 @@
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/
###< symfony/framework-bundle ###
###> phpunit/phpunit ###
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###
###> symfony/phpunit-bridge ###
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###
###> symfony/webpack-encore-bundle ###
/node_modules/
/public/build/
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
venv
wiki_compare/.env
wiki_compare/*.png
wiki_compare/*.json
public/*.json

8
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="framework" type="frameworkType"/>
<xs:complexType name="commandType">
<xs:all>
<xs:element type="xs:string" name="name" minOccurs="1" maxOccurs="1"/>
<xs:element type="xs:string" name="params" minOccurs="0" maxOccurs="1"/>
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
<xs:element type="optionsBeforeType" name="optionsBefore" minOccurs="0" maxOccurs="1"/>
</xs:all>
</xs:complexType>
<xs:complexType name="frameworkType">
<xs:sequence>
<xs:element type="xs:string" name="extraData" minOccurs="0" maxOccurs="1"/>
<xs:element type="commandType" name="command" maxOccurs="unbounded" minOccurs="0"/>
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute type="xs:string" name="name" use="required"/>
<xs:attribute type="xs:string" name="invoke" use="required"/>
<xs:attribute type="xs:string" name="alias" use="required"/>
<xs:attribute type="xs:boolean" name="enabled" use="required"/>
<xs:attribute type="xs:integer" name="version" use="required"/>
<xs:attribute type="xs:string" name="frameworkId" use="optional"/>
</xs:complexType>
<xs:complexType name="optionsBeforeType">
<xs:sequence>
<xs:element type="optionType" name="option" maxOccurs="unbounded" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="optionType">
<xs:sequence>
<xs:element type="xs:string" name="help" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute type="xs:string" name="name" use="required"/>
<xs:attribute type="xs:string" name="shortcut" use="optional"/>
<xs:attribute name="pattern" use="optional">
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="space"/>
<xs:enumeration value="equals"/>
<xs:enumeration value="unknown"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:schema>

View file

@ -0,0 +1,21 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="7">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
<item index="3" class="java.lang.String" itemvalue="noscript" />
<item index="4" class="java.lang.String" itemvalue="embed" />
<item index="5" class="java.lang.String" itemvalue="script" />
<item index="6" class="java.lang.String" itemvalue="Placeholder" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/qualiwiki.iml" filepath="$PROJECT_DIR$/.idea/qualiwiki.iml" />
</modules>
</component>
</project>

158
.idea/php.xml generated Normal file
View file

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCSFixerOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PHPCodeSnifferOptionsConfiguration">
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpIncludePathManager">
<include_path>
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-messenger" />
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
<path value="$PROJECT_DIR$/vendor/symfony/expression-language" />
<path value="$PROJECT_DIR$/vendor/symfony/mailer" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-icu" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
<path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
<path value="$PROJECT_DIR$/vendor/psr/cache" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/vendor/symfony/asset-mapper" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/psr/link" />
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
<path value="$PROJECT_DIR$/vendor/psr/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-turbo" />
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
<path value="$PROJECT_DIR$/vendor/symfony/debug-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
<path value="$PROJECT_DIR$/vendor/symfony/stimulus-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
<path value="$PROJECT_DIR$/vendor/symfony/web-link" />
<path value="$PROJECT_DIR$/vendor/monolog/monolog" />
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
<path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<path value="$PROJECT_DIR$/vendor/symfony/intl" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
<path value="$PROJECT_DIR$/vendor/symfony/notifier" />
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
<path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/symfony/form" />
<path value="$PROJECT_DIR$/vendor/symfony/messenger" />
<path value="$PROJECT_DIR$/vendor/symfony/config" />
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bridge" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
<path value="$PROJECT_DIR$/vendor/symfony/monolog-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php83" />
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.3">
<option name="suggestChangeDefaultLanguageLevel" value="false" />
</component>
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
<component name="PhpUnit">
<phpunit_settings>
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
</phpunit_settings>
</component>
<component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" />
</component>
</project>

140
.idea/qualiwiki.iml generated Normal file
View file

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="App\" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="App\Tests\" />
<excludeFolder url="file://$MODULE_DIR$/public/build" />
<excludeFolder url="file://$MODULE_DIR$/var" />
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/collections" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/dbal" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/deprecations" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/event-manager" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/inflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/instantiator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/lexer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/migrations" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/orm" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/persistence" />
<excludeFolder url="file://$MODULE_DIR$/vendor/doctrine/sql-formatter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/egulias/email-validator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/masterminds/html5" />
<excludeFolder url="file://$MODULE_DIR$/vendor/monolog/monolog" />
<excludeFolder url="file://$MODULE_DIR$/vendor/myclabs/deep-copy" />
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/php-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/manifest" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phar-io/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-common" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/reflection-docblock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpdocumentor/type-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpstan/phpdoc-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-code-coverage" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-file-iterator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-invoker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-text-template" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/php-timer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/phpunit/phpunit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/clock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/link" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/cli-parser" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/comparator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/complexity" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/diff" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/environment" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/exporter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/global-state" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/lines-of-code" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-enumerator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/object-reflector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/recursion-context" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/type" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/staabm/side-effects-detector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/asset" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/asset-mapper" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/browser-kit" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/cache-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/clock" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/config" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/debug-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dependency-injection" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/doctrine-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/doctrine-messenger" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dom-crawler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/dotenv" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/error-handler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/expression-language" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/filesystem" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/finder" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/flex" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/form" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/framework-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-foundation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-kernel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/intl" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mailer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/maker-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/messenger" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/mime" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/monolog-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/monolog-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/notifier" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/options-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/password-hasher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-icu" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php83" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php84" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-access" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/property-info" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/runtime" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-core" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-csrf" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/security-http" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/serializer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stimulus-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/stopwatch" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/twig-bridge" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/twig-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/type-info" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/ux-turbo" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/validator" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-exporter" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/web-link" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/web-profiler-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/yaml" />
<excludeFolder url="file://$MODULE_DIR$/vendor/theseer/tokenizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/extra-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/twig" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/symfony2.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Symfony2PluginSettings">
<option name="pluginEnabled" value="true" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -0,0 +1,34 @@
# QualiWiki
Outil de mise en qualité du wiki OpenStreetMap.
plusieurs pages:
- laccueil liste les pages par ordre de manque de mise à jour du plus grave au moins grave parmi les pages de tags très utilisés selon taginfo.
- on y voit aussi une liste de pages arbitrairement suivies, dont celle proposée par @AdrienHegy , on peut en mettre dautres.
- des pages fr sans traduction en anglais
- des pages englishes sans traduction fr
- un graphe de répartition de la décrépitude en fin de page.
- chaque page peut être comparée entre version fr et englishe. on y trouve une liste des sections, des images, des propositions de correction de la page française par Grammalecte. Exemple ici où on a clairement plus de contenu en version Française quen Anglais: Comparaison Wiki OSM - Tag:amenity=charging_station , cas inverse, la page de procédé de proposition Comparaison Wiki OSM - Proposal process qui est bien plus riche en Anglais quen Français.
- on a une suggestion de page au hasard pour voir à quel point elle manque de fraîcheur
- propositions archivées: une étude des 200 propositions de tags, de leurs status et de leurs votes avec quelques stats.
- changements récents: une vue globale des dernières modifications de lannée, ce qui permet de voir qui sont les gens qui potassent le wiki régulièrement.
- les propositions en cours de vote avec un graphe des votes
On peut aussi cliquer sur plusieurs trucs dans la page de comparaison pour copier ou rechercher dans le wiki.
Havez fun.
# installation
npm install
npm run build
composer install
pas besoin de base de données, les infos sont stockées dans des fichiers json suite au lancement de scripts de wiki_compare.
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# liens
sujet sur le forum osm fr: https://forum.openstreetmap.fr/t/fabriquer-un-outil-de-qualite-pour-le-wiki-osm/36814
- Tykayn - Cipherbliss.com

0
README.md~ Normal file
View file

360
assets/app.js Normal file
View file

@ -0,0 +1,360 @@
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.css';
import jQuery from 'jquery';
window.$ = jQuery;
window.jQuery = jQuery;
import 'tablesort/tablesort.css';
// start the Stimulus application
// import './bootstrap';
import './utils.js';
import './opening_hours.js';
import './josm.js';
import './edit.js';
import './table-map-toggles.js';
import './stats-charts.js';
import './dashboard-charts.js';
import Chart from 'chart.js/auto';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import maplibregl from 'maplibre-gl';
import {
genererCouleurPastel,
setupCitySearch,
handleAddCityFormSubmit,
enableLabourageForm,
getLabourerUrl,
adjustListGroupFontSize,
toggleCompletionInfo,
updateMapHeightForLargeScreens
} from './utils.js';
import tableSortJs from 'table-sort-js/table-sort.js';
import 'chartjs-adapter-date-fns';
console.log('TableSort', tableSortJs)
// Charger table-sortable (version non minifiée locale)
// import '../assets/js/table-sortable.js';
window.Chart = Chart;
window.genererCouleurPastel = genererCouleurPastel;
window.setupCitySearch = setupCitySearch;
window.handleAddCityFormSubmit = handleAddCityFormSubmit;
window.getLabourerUrl = getLabourerUrl;
window.ChartDataLabels = ChartDataLabels;
window.maplibregl = maplibregl;
window.toggleCompletionInfo = toggleCompletionInfo;
window.updateMapHeightForLargeScreens = updateMapHeightForLargeScreens;
// window.Tablesort = Tablesort;
Chart.register(ChartDataLabels);
// Attendre le chargement du DOM
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded');
if(updateMapHeightForLargeScreens){
window.addEventListener('resize', updateMapHeightForLargeScreens);
}
const randombg = genererCouleurPastel();
// Appliquer la couleur au body
document.querySelectorAll('body, .edit-land, .body-landing').forEach(element => {
element.style.backgroundColor = randombg;
});
// Gestion du bouton pour afficher tous les champs
const btnShowAllFields = document.querySelector('#showAllFields');
if (btnShowAllFields) {
console.log('btnShowAllFields détecté');
btnShowAllFields.addEventListener('click', () => {
// Sélectionner tous les inputs dans le formulaire
const form = document.querySelector('form');
if (form) {
// Sélectionner tous les inputs sauf #validation_messages
const hiddenInputs = form.querySelectorAll('#advanced_tags');
hiddenInputs.forEach(input => {
input.classList.toggle('d-none');
});
}
});
}
const btnClosedCommerce = document.querySelector('#closedCommerce');
if (btnClosedCommerce) {
btnClosedCommerce.addEventListener('click', () => {
if (!confirm('Êtes-vous sûr de vouloir signaler ce commerce comme fermé ?')) {
return;
}
window.location.href = '/closed_commerce/' + document.getElementById('app_public_closed_commerce').value;
});
}
openingHoursFormManager.init();
// Vérifier si l'élément avec l'ID 'userChangesHistory' existe avant d'appeler la fonction
if (document.getElementById('userChangesHistory')) {
listChangesets();
} else {
console.log('userChangesHistory non trouvé');
}
document.querySelectorAll('input[type="text"]').forEach(input => {
input.addEventListener('blur', updateCompletionProgress);
});
const form = document.querySelector('form')
if (form) {
form.addEventListener('submit', check_validity);
updateCompletionProgress()
}
updateCompletionProgress();
// Focus sur le premier champ texte au chargement
// const firstTextInput = document.querySelector('input.form-control');
// if (firstTextInput) {
// firstTextInput.focus();
// console.log('focus sur le premier champ texte', firstTextInput);
// } else {
// console.log('pas de champ texte trouvé');
// }
parseCuisine();
// Modifier la fonction de recherche existante
const searchInput = document.getElementById('app_admin_labourer');
const suggestionList = document.getElementById('suggestionList');
if (searchInput && suggestionList) {
let timeoutId;
searchInput.addEventListener('input', () => {
clearTimeout(timeoutId);
const query = searchInput.value.trim();
if (query.length < 2) {
suggestionList.innerHTML = '';
return;
}
timeoutId = setTimeout(async () => {
const suggestions = await searchInseeCode(query);
suggestionList.innerHTML = '';
if (suggestions.length === 0) {
const li = document.createElement('li');
li.style.cssText = `
padding: 8px 12px;
color: #666;
font-style: italic;
`;
li.textContent = 'Aucun résultat trouvé';
suggestionList.appendChild(li);
return;
}
suggestions.forEach(suggestion => {
const li = document.createElement('li');
li.style.cssText = `
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #eee;
`;
li.textContent = suggestion.label;
li.addEventListener('mouseenter', () => {
li.style.backgroundColor = '#f0f0f0';
});
li.addEventListener('mouseleave', () => {
li.style.backgroundColor = 'white';
});
li.addEventListener('click', () => {
searchInput.value = suggestion.insee;
suggestionList.innerHTML = '';
labourer();
});
suggestionList.appendChild(li);
});
}, 300);
});
}
if(enableLabourageForm){
enableLabourageForm();
}
adjustListGroupFontSize('.list-group-item');
// Activer le tri naturel sur tous les tableaux avec la classe table-sort
if (tableSortJs) {
tableSortJs();
}else{
console.log('pas de tablesort')
}
// Initialisation du tri et filtrage sur les tableaux du dashboard et de la page stats
// if (document.querySelector('#dashboard-table')) {
// $('#dashboard-table').tableSortable({
// pagination: false,
// showPaginationLabel: true,
// searchField: '#dashboard-table-search',
// responsive: false
// });
// }
// if (document.querySelector('#stats-table')) {
// $('#stats-table').tableSortable({
// pagination: false,
// showPaginationLabel: true,
// searchField: '#stats-table-search',
// responsive: false
// });
// }
// Correction pour le formulaire de labourage
const labourerForm = document.getElementById('labourerForm');
if (labourerForm) {
labourerForm.addEventListener('submit', async function(e) {
e.preventDefault();
const zipInput = document.getElementById('selectedZipCode');
const cityInput = document.getElementById('citySearch');
let insee = zipInput.value;
if (!insee && cityInput && cityInput.value.trim().length > 0) {
// Recherche du code INSEE via l'API
const response = await fetch(`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(cityInput.value.trim())}&fields=nom,code&limit=1`);
const data = await response.json();
if (data.length > 0) {
insee = data[0].code;
}
}
if (insee) {
window.location.href = `/add-city-without-labourage/${insee}`;
} else {
alert('Veuillez sélectionner une ville valide.');
}
});
}
// Ajouter un écouteur pour l'événement 'load' de MapLibre afin d'ajuster la hauteur de la carte
if (window.maplibregl && document.getElementById('map')) {
// On suppose que la carte est initialisée ailleurs et accessible via window.mapInstance
// Sinon, on peut essayer de détecter l'instance automatiquement
let mapInstance = window.mapInstance;
if (!mapInstance && window.maplibreMap) {
mapInstance = window.maplibreMap;
}
// Si l'instance n'est pas trouvée, essayer de la récupérer via une variable globale courante
if (!mapInstance && window.map) {
mapInstance = window.map;
}
if (mapInstance && typeof mapInstance.on === 'function') {
mapInstance.on('load', function() {
updateMapHeightForLargeScreens();
});
}
}
//updateMapHeightForLargeScreens();
console.log('window.followupSeries',window.followupSeries)
if (!window.followupSeries) return;
const series = window.followupSeries;
// Données bornes de recharge
const chargingStationCount = (series['charging_station_count'] || []).map(point => ({ x: point.date, y: point.value }));
const chargingStationCompletion = (series['charging_station_completion'] || []).map(point => ({ x: point.date, y: point.value }));
// Données bornes incendie
const fireHydrantCount = (series['fire_hydrant_count'] || []).map(point => ({ x: point.date, y: point.value }));
const fireHydrantCompletion = (series['fire_hydrant_completion'] || []).map(point => ({ x: point.date, y: point.value }));
// Graphique bornes de recharge
const chargingStationChart = document.getElementById('chargingStationChart');
if (chargingStationChart) {
new Chart(chargingStationChart, {
type: 'line',
data: {
datasets: [
{
label: 'Nombre de bornes de recharge',
data: chargingStationCount,
borderColor: 'blue',
backgroundColor: 'rgba(0,0,255,0.1)',
fill: false,
yAxisID: 'y',
},
{
label: 'Complétion (%)',
data: chargingStationCompletion,
borderColor: 'green',
backgroundColor: 'rgba(0,255,0,0.1)',
fill: false,
yAxisID: 'y1',
}
]
},
options: {
parsing: false,
responsive: true,
scales: {
x: { type: 'time', time: { unit: 'day' }, title: { display: true, text: 'Date' } },
y: { beginAtZero: true, title: { display: true, text: 'Nombre' } },
y1: { beginAtZero: true, position: 'right', title: { display: true, text: 'Complétion (%)' }, grid: { drawOnChartArea: false } }
}
}
});
}
// Graphique bornes incendie
const fireHydrantChart = document.getElementById('fireHydrantChart');
if (fireHydrantChart) {
new Chart(fireHydrantChart, {
type: 'line',
data: {
datasets: [
{
label: 'Nombre de bornes incendie',
data: fireHydrantCount,
borderColor: 'red',
backgroundColor: 'rgba(255,0,0,0.1)',
fill: false,
yAxisID: 'y',
},
{
label: 'Complétion (%)',
data: fireHydrantCompletion,
borderColor: 'orange',
backgroundColor: 'rgba(255,165,0,0.1)',
fill: false,
yAxisID: 'y1',
}
]
},
options: {
parsing: false,
responsive: true,
scales: {
x: { type: 'time', time: { unit: 'day' }, title: { display: true, text: 'Date' } },
y: { beginAtZero: true, title: { display: true, text: 'Nombre' } },
y1: { beginAtZero: true, position: 'right', title: { display: true, text: 'Complétion (%)' }, grid: { drawOnChartArea: false } }
}
}
});
}
});

5
assets/bootstrap.js vendored Normal file
View file

@ -0,0 +1,5 @@
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);

15
assets/controllers.json Normal file
View file

@ -0,0 +1,15 @@
{
"controllers": {
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
},
"mercure-turbo-stream": {
"enabled": false,
"fetch": "eager"
}
}
},
"entrypoints": []
}

View file

@ -0,0 +1,79 @@
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
document.addEventListener('submit', function (event) {
generateCsrfToken(event.target);
}, true);
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
document.addEventListener('turbo:submit-start', function (event) {
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
Object.keys(h).map(function (k) {
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
});
});
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
document.addEventListener('turbo:submit-end', function (event) {
removeCsrfToken(event.detail.formSubmission.formElement);
});
export function generateCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
let csrfToken = csrfField.value;
if (!csrfCookie && nameCheck.test(csrfToken)) {
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
}
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
if (csrfCookie && tokenCheck.test(csrfToken)) {
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
export function generateCsrfHeaders (formElement) {
const headers = {};
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return headers;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
headers[csrfCookie] = csrfField.value;
}
return headers;
}
export function removeCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
/* stimulusFetch: 'lazy' */
export default 'csrf-protection-controller';

View file

@ -0,0 +1,16 @@
import { Controller } from '@hotwired/stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}

229
assets/dashboard-charts.js Normal file
View file

@ -0,0 +1,229 @@
// Bubble chart du dashboard avec option de taille de bulle proportionnelle ou égale
let bubbleChart = null; // Déclaré en dehors pour garder la référence
function waitForChartAndDrawBubble() {
if (!window.Chart || !window.ChartDataLabels) {
setTimeout(waitForChartAndDrawBubble, 50);
return;
}
const chartCanvas = document.getElementById('bubbleChart');
const toggle = document.getElementById('toggleBubbleSize');
function drawBubbleChart(proportional) {
// Détruire toute instance Chart.js existante sur ce canvas (Chart.js v3+)
const existing = window.Chart.getChart(chartCanvas);
if (existing) {
try { existing.destroy(); } catch (e) { console.warn('Erreur destroy Chart:', e); }
}
if (bubbleChart) {
try { bubbleChart.destroy(); } catch (e) { console.warn('Erreur destroy Chart:', e); }
bubbleChart = null;
}
// Forcer le canvas à occuper toute la largeur/hauteur du conteneur en pixels
if (chartCanvas && chartCanvas.parentElement) {
const parentRect = chartCanvas.parentElement.getBoundingClientRect();
console.log('parentRect', parentRect)
chartCanvas.width = (parentRect.width);
chartCanvas.height = (parentRect.height);
chartCanvas.style.width = parentRect.width + 'px';
chartCanvas.style.height = parentRect.height + 'px';
}
if(!getBubbleData){
console.log('pas de getBubbleData')
return ;
}
const bubbleChartData = getBubbleData(proportional);
if(!bubbleChartData){
console.log('pas de bubbleChartData')
return ;
}
// Calcul de la régression linéaire (moindres carrés)
// On ne fait la régression que si on veut, mais l'axe X = fraicheur, Y = complétion
const validPoints = bubbleChartData.filter(d => d.x !== null && d.y !== null);
const n = validPoints.length;
let regressionLine = null, slope = 0, intercept = 0;
if (n >= 2) {
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
validPoints.forEach(d => {
sumX += d.x;
sumY += d.y;
sumXY += d.x * d.y;
sumXX += d.x * d.x;
});
const meanX = sumX / n;
const meanY = sumY / n;
slope = (sumXY - n * meanX * meanY) / (sumXX - n * meanX * meanX);
intercept = meanY - slope * meanX;
const xMin = Math.min(...validPoints.map(d => d.x));
const xMax = Math.max(...validPoints.map(d => d.x));
regressionLine = [
{ x: xMin, y: slope * xMin + intercept },
{ x: xMax, y: slope * xMax + intercept }
];
}
window.Chart.register(window.ChartDataLabels);
bubbleChart = new window.Chart(chartCanvas.getContext('2d'), {
type: 'bubble',
data: {
datasets: [
{
label: 'Villes',
data: bubbleChartData,
backgroundColor: bubbleChartData.map(d => `rgba(94, 255, 121, ${d.completion / 100})`),
borderColor: 'rgb(94, 255, 121)',
datalabels: {
anchor: 'center',
align: 'center',
color: '#000',
display: true,
font: { weight: '400', size : "12px" },
formatter: (value, context) => {
return context.dataset.data[context.dataIndex].label;
}
}
},
regressionLine ? {
label: 'Régression linéaire',
type: 'line',
data: regressionLine,
borderColor: 'rgba(95, 168, 0, 0.7)',
borderWidth: 2,
pointRadius: 0,
fill: false,
order: 0,
tension: 0,
datalabels: { display: false }
} : null
].filter(Boolean)
},
options: {
plugins: {
datalabels: {
display: false
},
legend: { display: true },
tooltip: {
callbacks: {
label: (context) => {
const d = context.raw;
if (context.dataset.type === 'line') {
return `Régression: y = ${slope.toFixed(2)} × x + ${intercept.toFixed(2)}`;
}
return [
`${d.label}`,
`Fraîcheur moyenne: ${d.freshnessDays ? d.freshnessDays.toLocaleString() + ' jours' : 'N/A'}`,
`Complétion: ${d.y.toFixed(2)}%`,
`Population: ${d.population ? d.population.toLocaleString() : 'N/A'}`,
`Nombre de lieux: ${d.r.toFixed(2)}`,
`Budget: ${d.budget ? d.budget.toLocaleString() + ' €' : 'N/A'}`,
`Budget/habitant: ${d.budgetParHabitant ? d.budgetParHabitant.toFixed(2) + ' €' : 'N/A'}`,
`Budget/lieu: ${d.budgetParLieu ? d.budgetParLieu.toFixed(2) + ' €' : 'N/A'}`
];
}
}
}
},
scales: {
x: {
type: 'linear',
title: { display: true, text: 'Fraîcheur moyenne (jours, plus petit = plus récent)' }
},
y: {
title: { display: true, text: 'Taux de complétion (%)' },
min: 0,
max: 100
}
}
}
});
// Ajout du clic sur une bulle
chartCanvas.onclick = function(evt) {
const points = bubbleChart.getElementsAtEventForMode(evt, 'nearest', { intersect: true }, true);
if (points.length > 0) {
const firstPoint = points[0];
const dataIndex = firstPoint.index;
const stat = window.statsDataForBubble[dataIndex];
if (stat && stat.zone) {
window.location.href = '/admin/stats/' + stat.zone;
}
}
};
}
// Initial draw
console.log('[bubble chart] Initialisation avec taille proportionnelle ?', toggle?.checked);
if(drawBubbleChart){
drawBubbleChart(toggle && toggle.checked);
// Listener
toggle?.addEventListener('change', function() {
console.log('[bubble chart] Toggle changé, taille proportionnelle ?', toggle?.checked);
drawBubbleChart(toggle?.checked);
});
}
}
function getBubbleData(proportional) {
// Générer les données puis trier par rayon décroissant
const data = window.statsDataForBubble?.map(stat => {
const population = parseInt(stat.population, 10);
const placesCount = parseInt(stat.placesCount, 10);
const completion = parseInt(stat.completionPercent, 10);
// Fraîcheur moyenne : âge moyen en jours (plus récent à droite)
let freshnessDays = null;
if (stat.osmDataDateAvg) {
const now = new Date();
const avgDate = new Date(stat.osmDataDateAvg);
freshnessDays = Math.round((now - avgDate) / (1000 * 60 * 60 * 24));
}
// Pour l'axe X, on veut que les plus récents soient à droite (donc X = -freshnessDays)
const x = freshnessDays !== null ? -freshnessDays : 0;
// Budget
const budget = stat.budgetAnnuel ? parseFloat(stat.budgetAnnuel) : null;
const budgetParHabitant = (budget && population) ? budget / population : null;
const budgetParLieu = (budget && placesCount) ? budget / placesCount : null;
return {
x: x,
y: completion,
r: proportional ? Math.sqrt(placesCount) * 2 : 12,
label: stat.name,
completion: stat.completionPercent || 0,
zone: stat.zone,
budget,
budgetParHabitant,
budgetParLieu,
population,
placesCount,
freshnessDays
};
});
// Trier du plus gros au plus petit rayon
if(data){
data.sort((a, b) => b.r - a.r);
}
return data;
}
document.addEventListener('DOMContentLoaded', function() {
waitForChartAndDrawBubble();
// Forcer deleteMissing=1 sur le formulaire de labourage
const labourerForm = document.getElementById('labourerForm');
if (labourerForm) {
labourerForm.addEventListener('submit', function(e) {
e.preventDefault();
const zipCode = document.getElementById('selectedZipCode').value;
if (zipCode) {
window.location.href = '/admin/labourer/' + zipCode + '?deleteMissing=1';
}
});
}
});

126
assets/edit.js Normal file
View file

@ -0,0 +1,126 @@
/**
* mettre à jour la barre de progression
* pour le formulaire de modification
*/
function updateCompletionProgress() {
const inputs = document.querySelectorAll('input[data-important]');
let filledInputs = 0;
let totalInputs = inputs.length;
let missingFields = [];
inputs.forEach(input => {
if (input.value.trim() !== '') {
filledInputs++;
} else {
// Get the field label or name for display in the popup
let fieldName = '';
const label = input.closest('.row')?.querySelector('.form-label, .label-translated');
if (label) {
fieldName = label.textContent.trim();
} else {
// If no label found, try to get a meaningful name from the input
const name = input.getAttribute('name');
if (name) {
// Extract field name from the attribute (e.g., commerce_tag_value__contact:email -> Email)
const parts = name.split('__');
if (parts.length > 1) {
fieldName = parts[1].replace('commerce_tag_value_', '').replace('contact:', '');
// Capitalize first letter
fieldName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
} else {
fieldName = name;
}
}
}
missingFields.push(fieldName || input.getAttribute('name') || 'Champ inconnu');
}
});
const completionPercentage = (filledInputs / totalInputs) * 100;
const progressBar = document.querySelector('#completion_progress .progress-bar');
if (progressBar) {
progressBar.style.width = completionPercentage + '%';
progressBar.setAttribute('aria-valuenow', completionPercentage);
// Create the completion display with a clickable question mark
const displayElement = document.querySelector('#completion_display');
// Format missing fields as an HTML list for better readability
let missingFieldsContent = '';
if (missingFields.length > 0) {
missingFieldsContent = '<ul class="list-unstyled mb-0">';
// Filter out empty or undefined field names and sort them alphabetically
missingFields
.filter(field => field && field.trim() !== '')
.sort()
.forEach(field => {
missingFieldsContent += `<li><i class="bi bi-exclamation-circle text-warning"></i> ${field}</li>`;
});
missingFieldsContent += '</ul>';
} else {
missingFieldsContent = 'Tous les champs importants sont remplis !';
}
displayElement.innerHTML = `Votre commerce est complété à ${Math.round(completionPercentage)}% <a href="#" class="missing-fields-info badge rounded-pill bg-warning text-dark ms-1" style="text-decoration: none; font-weight: bold;" data-bs-toggle="popover" data-bs-placement="bottom" title="Champs manquants" data-bs-html="true" data-bs-content="${missingFieldsContent.replace(/"/g, '&quot;')}">?</a>`;
// Initialize the Bootstrap popover
const popoverTrigger = displayElement.querySelector('.missing-fields-info');
if (popoverTrigger) {
// Add click handler to focus on the first missing field
popoverTrigger.addEventListener('click', function(e) {
e.preventDefault(); // Prevent scrolling to top
// Find the first missing field
const missingInput = document.querySelector('input[data-important]:not(.good_filled)');
if (missingInput) {
// Focus on the first missing field
missingInput.focus();
// Scroll to the field if needed
missingInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
// Use setTimeout to ensure this runs after the current execution context
setTimeout(() => {
if (typeof bootstrap !== 'undefined' && bootstrap.Popover) {
// Destroy existing popover if any
const existingPopover = bootstrap.Popover.getInstance(popoverTrigger);
if (existingPopover) {
existingPopover.dispose();
}
// Initialize new popover
new bootstrap.Popover(popoverTrigger, {
html: true,
trigger: 'click',
container: 'body'
});
} else {
console.warn('Bootstrap popover not available');
}
}, 0);
}
}
}
function parseCuisine() {
const cuisineInput = document.querySelector('input[name="commerce_tag_value__cuisine"]');
// Récupérer tous les checkboxes de type de cuisine
const cuisineCheckboxes = document.querySelectorAll('input[name="cuisine_type"]');
// Ajouter un écouteur d'événement sur chaque checkbox
cuisineCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => {
// Récupérer toutes les checkboxes cochées
const checkedCuisines = Array.from(document.querySelectorAll('input[name="cuisine_type"]:checked'))
.map(input => input.value);
// Mettre à jour l'input avec les valeurs séparées par des points-virgules
cuisineInput.value = checkedCuisines.join(';');
});
});
}
window.updateCompletionProgress = updateCompletionProgress;
window.parseCuisine = parseCuisine;

77
assets/josm.js Normal file
View file

@ -0,0 +1,77 @@
/**
* Ouvre les éléments sélectionnés dans JOSM
* @param {Object} map - L'instance de la carte MapLibre
* @param {boolean} map_is_loaded - État de chargement de la carte
* @param {Array|Object} osmElements - Élément(s) OSM à ouvrir (peut être un tableau d'objets ou un objet unique)
* @returns {void}
*/
function openInJOSM(map, map_is_loaded, osmElements) {
if (!map_is_loaded) {
alert('Veuillez attendre que la carte soit chargée');
return;
}
if (!osmElements || (Array.isArray(osmElements) && osmElements.length === 0)) {
alert('Aucun élément à ouvrir dans JOSM');
return;
}
const bounds = map.getBounds();
// Convertir en tableau si c'est un seul élément
const elements = Array.isArray(osmElements) ? osmElements : [osmElements];
// Séparer les éléments par type
const nodeIds = [];
const wayIds = [];
const relationIds = [];
elements.forEach(element => {
if (typeof element === 'string') {
// Si c'est juste un ID, on le traite comme un node par défaut
nodeIds.push(element);
} else {
// Sinon on utilise le type spécifié
switch (element.osm_type) {
case 'node':
nodeIds.push(element.osm_id);
break;
case 'way':
wayIds.push(element.osm_id);
break;
case 'relation':
relationIds.push(element.osm_id);
break;
}
}
});
// Construire les paramètres de sélection
const selectParams = [];
if (nodeIds.length > 0) selectParams.push(`node${nodeIds.join(',')}`);
if (wayIds.length > 0) selectParams.push(`way${wayIds.join(',')}`);
if (relationIds.length > 0) selectParams.push(`relation${relationIds.join(',')}`);
// Construire l'URL JOSM avec les paramètres de la boîte englobante
const josmUrl = `http://localhost:8111/load_and_zoom?` +
`left=${bounds.getWest()}&` +
`right=${bounds.getEast()}&` +
`top=${bounds.getNorth()}&` +
`bottom=${bounds.getSouth()}&` +
`select=${selectParams.join(',')}`;
// Utiliser le bouton caché pour ouvrir JOSM
// Créer un élément <a> temporaire
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.href = josmUrl;
console.log('josmUrl', josmUrl);
tempLink.click();
document.body.removeChild(tempLink);
}
window.openInJOSM = openInJOSM;

0
assets/js/map-utils.js Normal file
View file

File diff suppressed because one or more lines are too long

0
assets/js/table-sortable.min.js vendored Normal file
View file

683
assets/opening_hours.js Normal file
View file

@ -0,0 +1,683 @@
/**
* Gestion du formulaire d'horaires d'ouverture
*
*/
const openingHoursFormManager = {
defaultOpeningHours: '',
inputSelector: '',
init: function (inputSelector = 'input[name="custom__opening_hours"]') {
// Rechercher l'élément par son attribut name plutôt que par son id
this.setInputSelector(inputSelector);
const openingHoursInput = document.querySelector(inputSelector);
if (!openingHoursInput) {
console.warn('Élément ', inputSelector, ' non trouvé');
return;
}
this.defaultOpeningHours = openingHoursInput.value;
// Créer la div de rendu si elle n'existe pas
let renderDiv = document.getElementById('opening_hours_render');
if (!renderDiv) {
renderDiv = document.createElement('div');
renderDiv.id = 'opening_hours_render';
renderDiv.classList.add('mt-4');
openingHoursInput.parentNode.insertBefore(renderDiv, openingHoursInput.nextSibling);
}
this.makeForm(inputSelector);
if (this.defaultOpeningHours !== '') {
this.parseOpeningHoursValue(inputSelector);
}
// Ajouter un écouteur d'événement keyup sur l'input des horaires
openingHoursInput.addEventListener('keyup', () => {
this.defaultOpeningHours = openingHoursInput.value;
this.parseOpeningHoursValue(inputSelector);
});
},
setInputSelector: function (inputSelector) {
this.inputSelector = inputSelector;
},
/**
* convertir les checkboxes et inputs en horaires OSM dans l'input de référence
* @param {string} inputSelector
*/
parseOpeningHoursValue: function (inputSelector = 'input[name="custom__opening_hours"]') {
// Analyser la chaîne d'horaires d'ouverture
const parsedOpeningHours = [];
// Masquer toutes les plages horaires par défaut
const allDayContainers = document.querySelectorAll('.jour-container');
allDayContainers.forEach(container => {
const checkbox = container.querySelector('input[type="checkbox"]');
const horairesContainer = container.querySelector('.horaires-container');
checkbox.checked = false;
horairesContainer.classList.add('d-none');
});
if (this.defaultOpeningHours) {
// Diviser les différentes règles (séparées par des points-virgules)
const rules = this.defaultOpeningHours.split(';').map(r => r.trim());
rules.forEach(rule => {
// Extraire les jours et les heures
const parts = rule.split(' ').filter(Boolean);
const days = parts[0];
const hours = parts.slice(1).join(' ');
// Convertir les jours en français
const daysMap = {
'Mo': 'lundi',
'Tu': 'mardi',
'We': 'mercredi',
'Th': 'jeudi',
'Fr': 'vendredi',
'Sa': 'samedi',
'Su': 'dimanche'
};
// Gérer les plages de jours (ex: Mo-Fr)
if (days.includes('-')) {
const [start, end] = days.split('-');
const startIndex = Object.keys(daysMap).indexOf(start);
const endIndex = Object.keys(daysMap).indexOf(end);
const dayRange = [];
for (let i = startIndex; i <= endIndex; i++) {
const day = Object.keys(daysMap)[i];
dayRange.push(day);
// Cocher la case du jour
const checkbox = document.querySelector(`#jour-${daysMap[day]}`);
if (checkbox) {
checkbox.checked = true;
const horairesContainer = checkbox.closest('.jour-container').querySelector('.horaires-container');
horairesContainer.classList.remove('d-none');
// Gérer le cas "fermé"
if (hours === 'off') {
const fermeCheckbox = horairesContainer.querySelector(`input[name="${daysMap[day]}-ferme"]`);
if (fermeCheckbox) {
fermeCheckbox.checked = true;
// Décocher les plages horaires
const plageInputs = horairesContainer.querySelectorAll('.time-range input[type="checkbox"]');
plageInputs.forEach(plageInput => {
plageInput.checked = false;
plageInput.dispatchEvent(new Event('change'));
});
}
} else {
// Décocher la deuxième plage si elle n'est pas présente dans l'input
if (hours && !hours.includes(',')) {
const plage2Checkbox = horairesContainer.querySelector(`input[name="${daysMap[day]}-plage2-active"]`);
if (plage2Checkbox) {
plage2Checkbox.checked = false;
}
}
// Remplir la première plage horaire si spécifiée
if (hours) {
const [startTime, endTime] = hours.split('-');
if (startTime && endTime) {
const [startHour, startMinute] = startTime.split(':');
const [endHour, endMinute] = endTime.split(':');
const startHourInput = horairesContainer.querySelector(`input[name="${daysMap[day]}-plage1-start-hour"]`);
const startMinuteInput = horairesContainer.querySelector(`input[name="${daysMap[day]}-plage1-start-minute"]`);
const endHourInput = horairesContainer.querySelector(`input[name="${daysMap[day]}-plage1-end-hour"]`);
const endMinuteInput = horairesContainer.querySelector(`input[name="${daysMap[day]}-plage1-end-minute"]`);
if (startHourInput) startHourInput.value = startHour;
if (startMinuteInput) startMinuteInput.value = startMinute;
if (endHourInput) endHourInput.value = endHour;
if (endMinuteInput) endMinuteInput.value = endMinute;
}
}
}
}
}
parsedOpeningHours.push({
days: dayRange,
hours: hours
});
} else {
// Jour unique
const day = daysMap[days];
const checkbox = document.querySelector(`#jour-${day}`);
if (checkbox) {
checkbox.checked = true;
const horairesContainer = checkbox.closest('.jour-container').querySelector('.horaires-container');
horairesContainer.classList.remove('d-none');
// Gérer le cas "fermé"
if (hours === 'off') {
const fermeCheckbox = horairesContainer.querySelector(`input[name="${day}-ferme"]`);
if (fermeCheckbox) {
fermeCheckbox.checked = true;
// Décocher les plages horaires
const plageInputs = horairesContainer.querySelectorAll('.time-range input[type="checkbox"]');
plageInputs.forEach(plageInput => {
plageInput.checked = false;
plageInput.dispatchEvent(new Event('change'));
});
}
} else {
// Décocher la deuxième plage si elle n'est pas présente dans l'input
if (hours && !hours.includes(',')) {
const plage2Checkbox = horairesContainer.querySelector(`input[name="${day}-plage2-active"]`);
if (plage2Checkbox) {
plage2Checkbox.checked = false;
}
}
// Remplir la première plage horaire si spécifiée
if (hours) {
const [startTime, endTime] = hours.split('-');
if (startTime && endTime) {
const [startHour, startMinute] = startTime.split(':');
const [endHour, endMinute] = endTime.split(':');
const startHourInput = horairesContainer.querySelector(`input[name="${day}-plage1-start-hour"]`);
const startMinuteInput = horairesContainer.querySelector(`input[name="${day}-plage1-start-minute"]`);
const endHourInput = horairesContainer.querySelector(`input[name="${day}-plage1-end-hour"]`);
const endMinuteInput = horairesContainer.querySelector(`input[name="${day}-plage1-end-minute"]`);
if (startHourInput) startHourInput.value = startHour;
if (startMinuteInput) startMinuteInput.value = startMinute;
if (endHourInput) endHourInput.value = endHour;
if (endMinuteInput) endMinuteInput.value = endMinute;
}
}
}
}
parsedOpeningHours.push({
days: [days],
hours: hours
});
}
});
}
this.renderOpeningHours(parsedOpeningHours);
console.log(parsedOpeningHours);
},
makeForm: function (inputSelector = 'input[name="custom__opening_hours"]') {
const customOpeningHours = document.querySelector(inputSelector);
console.log('makeForm customOpeningHours', customOpeningHours);
if (customOpeningHours) {
// Créer un conteneur flex pour aligner l'input et le formulaire
const container = document.createElement('div');
container.classList.add('d-flex', 'flex-column', 'flex-md-row', 'align-items-start', 'gap-3', 'w-100');
// Créer le formulaire
const form = document.createElement('form');
form.id = 'app_public_opening_hours';
form.classList.add('mt-3', 'flex-grow-1');
// Créer les cases à cocher pour chaque jour
const jours = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'];
const joursDiv = document.createElement('div');
joursDiv.classList.add('jours-ouverture', 'mb-4', 'row', 'mx-4');
jours.forEach(jour => {
const jourContainer = document.createElement('div');
jourContainer.classList.add('jour-container', 'col-12');
// Checkbox pour le jour
const divCheck = document.createElement('div');
divCheck.classList.add('form-check', 'mb-2');
const input = document.createElement('input');
input.type = 'checkbox';
input.id = `jour-${jour.toLowerCase()}`;
input.name = `jour-${jour.toLowerCase()}`;
input.classList.add('form-check-input');
const label = document.createElement('label');
label.htmlFor = `jour-${jour.toLowerCase()}`;
label.classList.add('form-check-label');
label.textContent = jour;
divCheck.appendChild(input);
divCheck.appendChild(label);
jourContainer.appendChild(divCheck);
// Conteneur pour les plages horaires
const horairesContainer = document.createElement('div');
horairesContainer.classList.add('horaires-container', 'ml-2', 'd-none', 'row');
// Option "fermé"
const fermeContainer = document.createElement('div');
fermeContainer.classList.add('col-12', 'mb-2');
const fermeCheck = document.createElement('div');
fermeCheck.classList.add('form-check');
const fermeInput = document.createElement('input');
fermeInput.type = 'checkbox';
fermeInput.id = `${jour.toLowerCase()}-ferme`;
fermeInput.name = `${jour.toLowerCase()}-ferme`;
fermeInput.classList.add('form-check-input');
const fermeLabel = document.createElement('label');
fermeLabel.htmlFor = `${jour.toLowerCase()}-ferme`;
fermeLabel.classList.add('form-check-label');
fermeLabel.textContent = 'Fermé';
fermeCheck.appendChild(fermeInput);
fermeCheck.appendChild(fermeLabel);
fermeContainer.appendChild(fermeCheck);
horairesContainer.appendChild(fermeContainer);
// Première plage horaire
const plage1 = this.createTimeRangeInputs(`${jour.toLowerCase()}-plage1`);
horairesContainer.appendChild(plage1);
// Deuxième plage horaire
const plage2 = this.createTimeRangeInputs(`${jour.toLowerCase()}-plage2`);
horairesContainer.appendChild(plage2);
jourContainer.appendChild(horairesContainer);
joursDiv.appendChild(jourContainer);
// Ajouter l'événement pour afficher/masquer les plages horaires
input.addEventListener('change', (e) => {
horairesContainer.classList.toggle('d-none', !e.target.checked);
this.convertToOSMOpeningHours(inputSelector);
});
// Ajouter l'événement pour gérer l'option "fermé"
fermeInput.addEventListener('change', (e) => {
const plageInputs = horairesContainer.querySelectorAll('.time-range input[type="checkbox"]');
plageInputs.forEach(plageInput => {
plageInput.checked = !e.target.checked;
plageInput.dispatchEvent(new Event('change'));
});
this.convertToOSMOpeningHours(inputSelector);
});
});
form.appendChild(joursDiv);
// Ajouter le formulaire au conteneur
container.appendChild(form);
// Insérer le conteneur après l'input original
const parent = customOpeningHours.parentNode;
if (parent) {
parent.insertBefore(container, customOpeningHours.nextSibling);
}
// Ajouter un debounce pour limiter les appels lors des modifications
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// Appliquer le debounce à la fonction de conversion
const debouncedConvert = debounce(() => {
this.convertToOSMOpeningHours();
}, 300);
// Ajouter les listeners sur tous les inputs
const allInputs = form.querySelectorAll('input');
allInputs.forEach(input => {
input.addEventListener('change', debouncedConvert);
input.addEventListener('input', debouncedConvert);
});
} else {
console.log('pas d input opening hours détecté')
}
},
createTimeRangeInputs: function (prefix) {
const container = document.createElement('div');
container.classList.add('time-range', 'mb-2', 'col-12', 'col-md-6');
// Case à cocher pour activer la plage
const checkboxContainer = document.createElement('div');
checkboxContainer.classList.add('form-check', 'mb-2');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `${prefix}-active`;
checkbox.name = `${prefix}-active`;
checkbox.classList.add('form-check-input');
checkbox.checked = true;
const checkboxLabel = document.createElement('label');
checkboxLabel.htmlFor = `${prefix}-active`;
checkboxLabel.classList.add('form-check-label');
checkboxLabel.textContent = '';
checkboxContainer.appendChild(checkbox);
checkboxContainer.appendChild(checkboxLabel);
container.appendChild(checkboxContainer);
// Conteneur pour les inputs d'horaires
const timeContainer = document.createElement('div');
timeContainer.classList.add('ms-4', 'row', 'g-2');
// Heure de début
const startContainer = document.createElement('div');
startContainer.classList.add('col-6', 'd-flex', 'align-items-center', 'gap-2', 'start-hour');
const startHour = document.createElement('input');
startHour.type = 'number';
startHour.min = '0';
startHour.max = '23';
startHour.classList.add('form-control', 'form-control-sm');
startHour.style.width = '60px';
startHour.placeholder = 'HH';
startHour.name = `${prefix}-start-hour`;
// Définir les horaires par défaut selon la plage
startHour.value = prefix.includes('plage1') ? '08' : '14';
const startMinute = document.createElement('input');
startMinute.type = 'number';
startMinute.min = '0';
startMinute.max = '59';
startMinute.classList.add('form-control', 'form-control-sm');
startMinute.style.width = '60px';
startMinute.placeholder = 'MM';
startMinute.name = `${prefix}-start-minute`;
startMinute.value = '00';
// Heure de fin
const endContainer = document.createElement('div');
endContainer.classList.add('col-6', 'd-flex', 'align-items-center', 'gap-2', 'end-hour');
const endHour = document.createElement('input');
endHour.type = 'number';
endHour.min = '0';
endHour.max = '23';
endHour.classList.add('form-control', 'form-control-sm');
endHour.style.width = '60px';
endHour.placeholder = 'HH';
endHour.name = `${prefix}-end-hour`;
// Définir les horaires par défaut selon la plage
endHour.value = prefix.includes('plage1') ? '12' : '18';
const endMinute = document.createElement('input');
endMinute.type = 'number';
endMinute.min = '0';
endMinute.max = '59';
endMinute.classList.add('form-control', 'form-control-sm');
endMinute.style.width = '60px';
endMinute.placeholder = 'MM';
endMinute.name = `${prefix}-end-minute`;
endMinute.value = '00';
// Créer le texte avec les horaires
const timeText = document.createElement('div');
timeText.classList.add('d-flex', 'align-items-center', 'gap-2', 'flex-wrap');
// Texte de début
const startText = document.createElement('span');
startText.textContent = 'de';
timeText.appendChild(startText);
timeText.appendChild(startHour);
timeText.appendChild(document.createTextNode(':'));
timeText.appendChild(startMinute);
// Texte du milieu
const middleText = document.createElement('span');
middleText.textContent = 'à';
timeText.appendChild(middleText);
// Texte de fin
timeText.appendChild(endHour);
timeText.appendChild(document.createTextNode(':'));
timeText.appendChild(endMinute);
timeContainer.appendChild(timeText);
container.appendChild(timeContainer);
// Fonction pour mettre à jour le style des inputs
const updateInputsStyle = (isActive) => {
const inputs = timeContainer.querySelectorAll('input');
inputs.forEach(input => {
if (isActive) {
input.classList.remove('bg-light', 'text-muted');
input.disabled = false;
} else {
input.classList.add('bg-light', 'text-muted');
input.disabled = true;
}
});
};
// Ajouter l'événement sur la checkbox
checkbox.addEventListener('change', (e) => {
updateInputsStyle(e.target.checked);
});
// Appliquer le style initial
updateInputsStyle(checkbox.checked);
return container;
},
convertToOSMOpeningHours: function (inputSelector = 'input[name="custom__opening_hours"]') {
const jours = {
'Lundi': 'Mo',
'Mardi': 'Tu',
'Mercredi': 'We',
'Jeudi': 'Th',
'Vendredi': 'Fr',
'Samedi': 'Sa',
'Dimanche': 'Su'
};
let joursSelectionnes = [];
let horairesParJour = {};
// Parcourir les checkboxes des jours
Object.keys(jours).forEach(jour => {
const checkbox = document.getElementById(`jour-${jour.toLowerCase()}`);
if (checkbox && checkbox.checked) {
joursSelectionnes.push(jours[jour]);
// Vérifier si le jour est marqué comme fermé
const fermeCheckbox = document.getElementById(`${jour.toLowerCase()}-ferme`);
if (fermeCheckbox && fermeCheckbox.checked) {
horairesParJour[jours[jour]] = 'off';
} else {
// Récupérer les horaires pour ce jour
const prefix = jour.toLowerCase();
const plage1 = this.getTimeRange(`${prefix}-plage1`);
const plage2 = this.getTimeRange(`${prefix}-plage2`);
let horaires = [];
if (plage1) horaires.push(plage1);
if (plage2) horaires.push(plage2);
if (horaires.length > 0) {
horairesParJour[jours[jour]] = horaires.join(',');
}
}
}
});
// Optimiser les plages horaires identiques consécutives
let optimizedRules = [];
let currentRule = null;
let currentDays = [];
let currentHours = null;
joursSelectionnes.forEach((jour, index) => {
const hours = horairesParJour[jour];
if (currentHours === null) {
// Première règle
currentHours = hours;
currentDays = [jour];
} else if (hours === currentHours) {
// Mêmes horaires que la règle en cours
currentDays.push(jour);
} else {
// Horaires différents, on finalise la règle en cours
if (currentDays.length > 0) {
optimizedRules.push({
days: currentDays,
hours: currentHours
});
}
// On commence une nouvelle règle
currentDays = [jour];
currentHours = hours;
}
// Si c'est le dernier jour, on finalise la règle en cours
if (index === joursSelectionnes.length - 1 && currentDays.length > 0) {
optimizedRules.push({
days: currentDays,
hours: currentHours
});
}
});
// Construire la chaîne au format OSM
let osmFormat = optimizedRules.map(rule => {
const days = rule.days.length > 1 ? `${rule.days[0]}-${rule.days[rule.days.length - 1]}` : rule.days[0];
return rule.hours ? `${days} ${rule.hours}` : days;
}).join('; ');
// Mettre à jour l'input custom__opening_hours
const customOpeningHours = document.querySelector(inputSelector);
if (customOpeningHours) {
customOpeningHours.value = osmFormat;
}
// Mettre à jour le rendu visuel
this.renderOpeningHours(optimizedRules);
},
renderOpeningHours: function (rules) {
const container = document.getElementById('opening_hours_render');
console.log('renderOpeningHours', rules);
if (!container) return;
// Vider le conteneur
container.innerHTML = '';
// Créer le style pour les sections d'ouverture
const style = document.createElement('style');
style.textContent = `
.opening-hours-day {
background-color: #f8f9fa;
border-radius: 4px;
margin-bottom: 8px;
padding: 8px;
position: relative;
height: 40px;
}
.opening-hours-time {
background-color: #d4edda;
border-radius: 4px;
position: absolute;
height: 24px;
top: 8px;
}
.opening-hours-label {
position: absolute;
left: 8px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
}
.opening-hours-closed {
background-color: #f8d7da;
}
`;
document.head.appendChild(style);
// Mapping des jours OSM vers français
const joursMap = {
'Mo': 'Lundi',
'Tu': 'Mardi',
'We': 'Mercredi',
'Th': 'Jeudi',
'Fr': 'Vendredi',
'Sa': 'Samedi',
'Su': 'Dimanche'
};
// Créer les lignes pour chaque jour
Object.entries(joursMap).forEach(([osmDay, frenchDay]) => {
const dayDiv = document.createElement('div');
dayDiv.classList.add('opening-hours-day');
// Ajouter le label du jour
const label = document.createElement('span');
label.classList.add('opening-hours-label');
label.textContent = frenchDay;
dayDiv.appendChild(label);
// Trouver les horaires pour ce jour
const rule = rules.find(r => r.days.includes(osmDay));
if (rule) {
if (rule.hours === 'off') {
// Jour fermé
dayDiv.classList.add('opening-hours-closed');
} else if (rule.hours) {
// Plages horaires spécifiques
const timeRanges = rule.hours.split(',');
timeRanges.forEach((timeRange, index) => {
const [start, end] = timeRange.split('-');
const [startHour, startMinute] = start.split(':');
const [endHour, endMinute] = end.split(':');
// Calculer la position et la largeur
const startMinutes = parseInt(startHour) * 60 + parseInt(startMinute);
const endMinutes = parseInt(endHour) * 60 + parseInt(endMinute);
const totalMinutes = 24 * 60;
const left = (startMinutes / totalMinutes) * 100;
const width = ((endMinutes - startMinutes) / totalMinutes) * 100;
const timeDiv = document.createElement('div');
timeDiv.classList.add('opening-hours-time');
timeDiv.style.left = `${left}%`;
timeDiv.style.width = `${width}%`;
timeDiv.title = `${start}-${end}`;
dayDiv.appendChild(timeDiv);
});
} else {
// Toute la journée
const timeDiv = document.createElement('div');
timeDiv.classList.add('opening-hours-time');
timeDiv.style.left = '0%';
timeDiv.style.width = '100%';
timeDiv.title = 'Toute la journée';
dayDiv.appendChild(timeDiv);
}
}
container.appendChild(dayDiv);
});
},
getTimeRange: function (prefix) {
const isActive = document.querySelector(`input[name="${prefix}-active"]`).checked;
if (!isActive) return null;
const startHour = document.querySelector(`input[name="${prefix}-start-hour"]`).value;
const startMinute = document.querySelector(`input[name="${prefix}-start-minute"]`).value;
const endHour = document.querySelector(`input[name="${prefix}-end-hour"]`).value;
const endMinute = document.querySelector(`input[name="${prefix}-end-minute"]`).value;
if (startHour && startMinute && endHour && endMinute) {
const start = `${startHour.padStart(2, '0')}:${startMinute.padStart(2, '0')}`;
const end = `${endHour.padStart(2, '0')}:${endMinute.padStart(2, '0')}`;
return `${start}-${end}`;
}
return null;
}
}
window.openingHoursFormManager = openingHoursFormManager;

44
assets/stats-charts.js Normal file
View file

@ -0,0 +1,44 @@
// Affichage du graphique de fréquence des mises à jour par trimestre sur la page stats d'une ville
document.addEventListener('DOMContentLoaded', function() {
function drawModificationsByQuarterChart() {
if (!window.Chart) {
setTimeout(drawModificationsByQuarterChart, 50);
return;
}
if (typeof window.modificationsByQuarter === 'undefined') return;
const modifData = window.modificationsByQuarter;
const modifLabels = Object.keys(modifData);
const modifCounts = Object.values(modifData);
const modifCanvas = document.getElementById('modificationsByQuarterChart');
if (modifCanvas && modifLabels.length > 0) {
new window.Chart(modifCanvas.getContext('2d'), {
type: 'bar',
data: {
labels: modifLabels,
datasets: [{
label: 'Nombre de lieux modifiés',
data: modifCounts,
backgroundColor: 'rgba(54, 162, 235, 0.7)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
title: { display: true, text: 'Fréquence des mises à jour par trimestre' }
},
scales: {
y: { beginAtZero: true, title: { display: true, text: 'Nombre de lieux' } },
x: { title: { display: true, text: 'Trimestre' } }
}
}
});
} else if (modifCanvas) {
modifCanvas.parentNode.innerHTML = '<div class="alert alert-info">Aucune donnée de modification disponible pour cette ville.</div>';
}
}
drawModificationsByQuarterChart();
});

192
assets/styles/app.css Normal file
View file

@ -0,0 +1,192 @@
body {
background-color: rgb(236, 236, 236);
transition: background-color ease 0.5s;
}
.body-landing .container {
background-color: white;
border-radius: 10px;
padding: 20px;
}
.main-footer {
padding-bottom: 20rem;
background-color: #dfe5eb;
}
#qrcode {
margin-bottom: 8rem;
}
input[data-important] {
border-color: #7a8fbb;
border-left-width: 5px;
}
input[data-important]:before {
content: ">" !important;
}
.filled, .good_filled {
border-color: rgba(0, 255, 0, 0.8) !important;
color: #082b0a !important;
}
.filled:hover, .good_filled:hover {
background-color: #d9ffd1 !important;
}
.no-name {
color: #df5a0d;
}
table {
max-height: 100vh;
max-width: 100vw;
}
table.js-sort-table th {
cursor: pointer;
}
table.js-sort-table th:hover {
background-color: #f0f0f0;
}
table.js-sort-table th:active {
background-color: #e0e0e0;
}
/*// formulaire de temps d'ouverture */
.form-check-input {
margin-right: 2rem;
}
.jour-container {
border: solid #dedede 1px;
}
#maploader {
position: relative;
top: 200px;
left: 50%;
z-index: 100;
}
.maplibregl-popup-content {
overflow: auto;
min-width: 300px;
max-height: 11rem !important;
}
.maplibregl-popup-content strong {
min-width: 10rem;
}
.maplibregl-popup-content table {
width: 100%;
max-height: 300px;
overflow: auto;
}
.maplibregl-popup-content h1,
.maplibregl-popup-content h2,
.maplibregl-popup-contenth3 {
font-size: 1rem;
}
.card {
overflow: auto;
}
#attribution {
font-size: 0.6rem;
}
#restaurant .form-check-label {
margin-top: 0;
display: block;
cursor: pointer;
}
#restaurant .form-check-label:hover {
background-color: #f0f0f0;
color: #1e40c6;
}
#advanced_tags {
border-left: solid 3px #ccc;
padding-left: 2rem;
}
.start-hour {
margin-left: -1rem;
}
.end-hour {
margin-left: -1rem;
}
.good_filled {
border-color: green;
}
.hidden {
display: none;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
}
.is-invalid {
border: 1px solid red;
}
.is-invalid #validation_messages {
color: red;
}
img {
max-width: 100%;
max-height: 400px;
}
#completionHistoryChart {
min-height: 500px;
}
@media (max-width: 768px) {
.form-label {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 1rem !important;
}
}
table tbody {
max-height: 700px;
overflow: auto;
}
#table_container, .table-container, #table-container {
max-height: 700px;
overflow: auto;
display: block;
border: solid 3px rgb(255, 255, 255);
}
#citySuggestions {
z-index: 10;
}
body .card:hover {
transform: none !important;
box-shadow: none !important;
}

View file

@ -0,0 +1,19 @@
// Gestion du tri des tableaux
// import Tablesort from 'tablesort';
document.addEventListener('DOMContentLoaded', () => {
// Gestion du toggle gouttes/ronds sur la carte
const toggle = document.getElementById('toggleMarkers');
if (toggle && window.updateMarkers) {
toggle.addEventListener('change', (e) => {
window.updateMarkers(e.target.value);
});
}
});
// Exposer une fonction pour (ré)appliquer le tri si besoin
export function applyTableSort() {
document.querySelectorAll('.js-sort-table').forEach(table => {
new window.Tablesort(table);
});
}

384
assets/utils.js Normal file
View file

@ -0,0 +1,384 @@
function colorHeadingTable() {
const headers = document.querySelectorAll('th');
headers.forEach(header => {
const text = header.textContent;
const match = text.match(/\((\d+)\s*\/\s*(\d+)\)/);
if (match) {
const [_, completed, total] = match;
const ratio = completed / total;
const alpha = ratio.toFixed(2);
header.style.backgroundColor = `rgba(154, 205, 50, ${alpha})`;
}
});
}
function check_validity(e) {
list_inputs_good_to_fill = [
'input[name="commerce_tag_value__contact:email"]',
'input[name="commerce_tag_value__contact:phone"]',
'input[name="commerce_tag_value__contact:website"]',
'input[name="commerce_tag_value__contact:mastodon"]',
'input[name="commerce_tag_value__address"]',
'input[name="custom_opening_hours"]',
'input[name="commerce_tag_value__contact:street"]',
'input[name="commerce_tag_value__contact:housenumber"]',
'input[name="custom__cuisine"]',
]
list_inputs_good_to_fill.forEach(selector => {
const input = document.querySelector(selector);
if (input) {
if (input.value.trim() !== '') {
input.classList.add('good_filled');
} else {
input.classList.remove('good_filled');
}
}
});
let errors = [];
document.querySelectorAll('.is-invalid').forEach(input => {
input.classList.remove('is-invalid');
});
const nameInput = document.querySelector('input[name="commerce_tag_value__name"]');
if (!nameInput.value.trim()) {
errors.push("Le nom de l'établissement est obligatoire");
nameInput.classList.add('is-invalid');
}
const emailInput = document.querySelector('input[name="commerce_tag_value__contact:email"]');
if (emailInput && emailInput.value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(emailInput.value)) {
errors.push("L'adresse email n'est pas valide");
emailInput.classList.add('is-invalid');
}
}
const phoneInput = document.querySelector('input[name="commerce_tag_value__contact:phone"]');
if (phoneInput && phoneInput.value) {
const phoneRegex = /^(\+33|0)[1-9](\d{2}){4}$/;
if (!phoneRegex.test(phoneInput.value.replace(/\s/g, ''))) {
errors.push("Le numéro de téléphone n'est pas valide");
phoneInput.classList.add('is-invalid');
}
}
if (errors.length > 0) {
e.preventDefault();
document.querySelector('#validation_messages').innerHTML = errors.join('<br>');
document.querySelector('#validation_messages').classList.add('is-invalid');
}
}
export const genererCouleurPastel = () => {
const r = Math.floor(Math.random() * 75 + 180);
const g = Math.floor(Math.random() * 75 + 180);
const b = Math.floor(Math.random() * 75 + 180);
return `rgb(${r}, ${g}, ${b})`;
};
async function searchInseeCode(query) {
try {
document.querySelector('#loading_search_insee').classList.remove('d-none');
const response = await fetch(`https://geo.api.gouv.fr/communes?nom=${query}&fields=nom,code,codesPostaux&limit=10`);
const data = await response.json();
document.querySelector('#loading_search_insee').classList.add('d-none');
return data.map(commune => ({
label: `${commune.nom} (${commune.codesPostaux.join(', ')}, code insee ${commune.code})`,
insee: commune.code,
postcodes: commune.codesPostaux
}));
} catch (error) {
console.error('Erreur lors de la recherche du code INSEE:', error);
return [];
}
}
export function updateMapHeightForLargeScreens() {
const mapFound = document.querySelector('#map');
const canvasFound = document.querySelector('#map canvas');
const newHeight = window.innerHeight * 0.5 + 'px'
if (mapFound && window.innerHeight > 800 && window.innerWidth > 800) {
mapFound.style.height = newHeight;
} else {
console.log('window.innerHeight', window.innerHeight);
}
}
async function listChangesets() {
const options = {
headers: {
'Accept': 'application/json'
}
};
const changesets = await fetch('https://api.openstreetmap.org/api/0.6/changesets?display_name=osm-commerce-fr', options);
const data = await changesets.json();
console.log(data.changesets.length);
const now = new Date();
const last24h = new Date(now - 24 * 60 * 60 * 1000);
const last7days = new Date(now - 7 * 24 * 60 * 60 * 1000);
const last30days = new Date(now - 30 * 24 * 60 * 60 * 1000);
const stats = {
last24h: 0,
last7days: 0,
last30days: 0
};
data.changesets.forEach(changeset => {
const changesetDate = new Date(changeset.closed_at);
if (changesetDate >= last24h) {
stats.last24h++;
}
if (changesetDate >= last7days) {
stats.last7days++;
}
if (changesetDate >= last30days) {
stats.last30days++;
}
});
const historyDiv = document.getElementById('userChangesHistory');
if (historyDiv) {
historyDiv.innerHTML = `
<div id="changesets_history">
<p>Changesets créés :</p>
<div class="row">
<div class="col-6">Dernières 24h :</div> <div class="col-6 text-right">${stats.last24h}</div>
<div class="col-6">7 derniers jours :</div> <div class="col-6 text-right">${stats.last7days}</div>
<div class="col-6">30 derniers jours :</div> <div class="col-6 text-right">${stats.last30days}</div>
</div>
</div>
`;
}
}
function openInPanoramax() {
const center = map.getCenter();
const zoom = map.getZoom();
const panoramaxUrl = `https://api.panoramax.xyz/?focus=map&map=${zoom}/${center.lat}/${center.lng}`;
window.open(panoramaxUrl);
}
export function enableLabourageForm() {
const citySearchInput = document.getElementById('citySearch');
const citySuggestionsList = document.getElementById('citySuggestions');
if (citySearchInput && citySuggestionsList) {
const form = citySearchInput.closest('form');
setupCitySearch('citySearch', 'citySuggestions', function (result_search) {
if (form) {
const labourageBtn = form.querySelector('button[type="submit"]');
if (labourageBtn) {
// Remplir le champ caché avec le code INSEE
const inseeInput = form.querySelector('#selectedZipCode');
if (inseeInput) {
inseeInput.value = result_search.insee;
}
// Changer l'action du formulaire pour pointer vers la bonne URL
form.action = getLabourerUrl(result_search);
// Changer le texte du bouton et le désactiver
labourageBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Ajout de ' + result_search.name + '...';
labourageBtn.disabled = true;
// Soumettre le formulaire
form.submit();
}
}else{
console.warn('pas de form pour labourage trouvé')
}
});
}else{
console.warn('pas de labourage citySearchInput citySuggestionsList trouvé', citySearchInput,citySuggestionsList )
}
}
export function setupCitySearch(inputId, suggestionListId, onSelect) {
const searchInput = document.getElementById(inputId);
const suggestionList = document.getElementById(suggestionListId);
if (!searchInput || !suggestionList) return;
let timeoutId = null;
let searchOngoing = false;
searchInput.addEventListener('keyup', function () {
clearTimeout(timeoutId);
const query = this.value.trim();
if (query.length < 3) {
clearSuggestions();
return;
}
timeoutId = setTimeout(() => {
if (!searchOngoing) {
searchOngoing = true;
performSearch(query).then(() => {
searchOngoing = false;
});
}
}, 300);
});
async function performSearch(query) {
try {
const response = await fetch(`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(query)}&fields=nom,code,codesPostaux&limit=5`);
const data = await response.json();
const citySuggestions = data.map(city => ({
name: city.nom,
insee: city.code,
postcodes: city.codesPostaux,
postcode: city.codesPostaux && city.codesPostaux.length > 0 ? city.codesPostaux[0] : '',
display_name: `${city.nom} (${city.codesPostaux && city.codesPostaux.length > 0 ? city.codesPostaux[0] : ''})`
}));
displaySuggestions(citySuggestions);
} catch (error) {
console.error("Erreur de recherche:", error);
}
}
function displaySuggestions(suggestions) {
clearSuggestions();
suggestions.forEach(suggestion => {
const item = document.createElement('div');
item.classList.add('suggestion-item');
// Nouveau rendu : nom en gras, INSEE et CP en petit/gris
item.innerHTML = `
<span class="suggestion-name" style="font-weight:bold;">${suggestion.name}</span><br>
<span class="suggestion-details" style="font-size:0.95em;color:#666;">
<span>INSEE&nbsp;: <b>${suggestion.insee}</b></span>
<span style="margin-left:12px;">CP&nbsp;: <b>${Array.isArray(suggestion.postcodes) ? suggestion.postcodes.join(', ') : suggestion.postcode}</b></span>
</span>
`;
item.addEventListener('click', () => {
searchInput.value = suggestion.name;
clearSuggestions();
if (onSelect) {
onSelect(suggestion);
}
});
suggestionList.appendChild(item);
});
suggestionList.style.display = 'block';
}
function clearSuggestions() {
suggestionList.innerHTML = '';
suggestionList.style.display = 'none';
}
document.addEventListener('click', (e) => {
if (!searchInput.contains(e.target) && !suggestionList.contains(e.target)) {
clearSuggestions();
}
});
}
export function getLabourerUrl(obj) {
if (obj && obj.insee) {
return `/add-city-without-labourage/${obj.insee}`;
}
return '#';
}
export function handleAddCityFormSubmit(event) {
event.preventDefault();
const zipCode = document.getElementById('selectedZipCode').value;
if (zipCode && zipCode.match(/^\d{5}$/)) {
window.location.href = `/add-city-without-labourage/${zipCode}`;
} else {
alert('Veuillez sélectionner une ville valide avec un code postal.');
}
}
export function colorizePercentageCells(selector, color = '154, 205, 50') {
document.querySelectorAll(selector).forEach(cell => {
const percentage = parseInt(cell.textContent.replace('%', ''), 10);
if (!isNaN(percentage)) {
const alpha = percentage / 100;
cell.style.backgroundColor = `rgba(${color}, ${alpha})`;
}
});
}
export function colorizePercentageCellsRelative(selector, color = '154, 205, 50') {
let min = Infinity;
let max = -Infinity;
const cells = document.querySelectorAll(selector);
cells.forEach(cell => {
const value = parseInt(cell.textContent.replace('%', ''), 10);
if (!isNaN(value)) {
min = Math.min(min, value);
max = Math.max(max, value);
}
});
if (max > min) {
cells.forEach(cell => {
const value = parseInt(cell.textContent.replace('%', ''), 10);
if (!isNaN(value)) {
const ratio = (value - min) / (max - min);
cell.style.backgroundColor = `rgba(${color}, ${ratio.toFixed(2)})`;
}
});
}
}
export function adjustListGroupFontSize(selector, minFont = 0.8, maxFont = 1.2) {
const listItems = document.querySelectorAll(selector);
if (listItems.length === 0) return;
let fontSize = maxFont;
const count = listItems.length;
if (count > 0) {
fontSize = Math.max(minFont, maxFont - (count - 5) * 0.05);
}
listItems.forEach(item => {
item.style.fontSize = fontSize + 'rem';
});
}
export function calculateCompletion(properties) {
let completed = 0;
const total = 7; // Nombre de critères
if (properties.name) completed++;
if (properties['addr:housenumber'] && properties['addr:street']) completed++;
if (properties.opening_hours) completed++;
if (properties.website || properties['contact:website']) completed++;
if (properties.phone || properties['contact:phone']) completed++;
if (properties.wheelchair) completed++;
return {
percentage: total > 0 ? (completed / total) * 100 : 0,
completed: completed,
total: total
};
}
export function toggleCompletionInfo() {
const content = document.getElementById('completionInfoContent');
const icon = document.getElementById('completionInfoIcon');
if (content) {
const isVisible = content.style.display === 'block';
content.style.display = isVisible ? 'none' : 'block';
if (icon) {
icon.classList.toggle('bi-chevron-down', isVisible);
icon.classList.toggle('bi-chevron-up', !isVisible);
}
}
}
window.check_validity = check_validity;
window.colorHeadingTable = colorHeadingTable;
window.openInPanoramax = openInPanoramax;
window.listChangesets = listChangesets;
window.adjustListGroupFontSize = adjustListGroupFontSize;
window.calculateCompletion = calculateCompletion;
window.toggleCompletionInfo = toggleCompletionInfo;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
assets/vendor/installed.php vendored Normal file
View file

@ -0,0 +1,22 @@
<?php return array (
'@hotwired/stimulus' =>
array (
'version' => '3.2.2',
'dependencies' =>
array (
),
'extraFiles' =>
array (
),
),
'@hotwired/turbo' =>
array (
'version' => '7.3.0',
'dependencies' =>
array (
),
'extraFiles' =>
array (
),
),
);

21
bin/console Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_dir(dirname(__DIR__).'/vendor')) {
throw new LogicException('Dependencies are missing. Try running "composer install".');
}
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return new Application($kernel);
};

23
bin/phpunit Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env php
<?php
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
if (PHP_VERSION_ID >= 80000) {
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
} else {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
}
} else {
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
}

View file

@ -0,0 +1,24 @@
qualiwiki.cipherbliss.com {
root * /home/poule/encrypted/stockage-syncable/www/development/html/qualiwiki/public
# serve files directly if they can be found (e.g. CSS or JS files in public/)
encode zstd gzip
file_server
# otherwise, use PHP-FPM (replace "unix//var/..." with "127.0.0.1:9000" when using TCP)
php_fastcgi unix//var/run/php/php8.4-fpm.sock {
# only fall back to root index.php aka front controller.
try_files {path} index.php
}
# Logs
log {
output file /var/log/caddy/access-qualiwiki.log
format console
}
@phpFile {
path *.php*
}
error @phpFile "Not found" 404
}

18
compose.override.yaml Normal file
View file

@ -0,0 +1,18 @@
services:
###> doctrine/doctrine-bundle ###
database:
ports:
- "5432"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
mailer:
image: axllent/mailpit
ports:
- "1025"
- "8025"
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
###< symfony/mailer ###

25
compose.yaml Normal file
View file

@ -0,0 +1,25 @@
services:
###> doctrine/doctrine-bundle ###
database:
image: postgres:${POSTGRES_VERSION:-16}-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
# You should definitely change the password in production
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
POSTGRES_USER: ${POSTGRES_USER:-app}
healthcheck:
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
timeout: 5s
retries: 5
start_period: 60s
volumes:
- database_data:/var/lib/postgresql/data:rw
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
# - ./docker/db/data:/var/lib/postgresql/data:rw
###< doctrine/doctrine-bundle ###
volumes:
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###

108
composer.json Normal file
View file

@ -0,0 +1,108 @@
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.2",
"ext-ctype": "*",
"ext-iconv": "*",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.15",
"doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.5",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.3",
"symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*",
"symfony/console": "7.3.*",
"symfony/doctrine-messenger": "7.3.*",
"symfony/dotenv": "7.3.*",
"symfony/expression-language": "7.3.*",
"symfony/flex": "^2",
"symfony/form": "7.3.*",
"symfony/framework-bundle": "7.3.*",
"symfony/http-client": "7.3.*",
"symfony/intl": "7.3.*",
"symfony/mailer": "7.3.*",
"symfony/mime": "7.3.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "7.3.*",
"symfony/process": "7.3.*",
"symfony/property-access": "7.3.*",
"symfony/property-info": "7.3.*",
"symfony/runtime": "7.3.*",
"symfony/security-bundle": "7.3.*",
"symfony/serializer": "7.3.*",
"symfony/stimulus-bundle": "^2.30",
"symfony/string": "7.3.*",
"symfony/translation": "7.3.*",
"symfony/twig-bundle": "7.3.*",
"symfony/ux-turbo": "^2.30",
"symfony/validator": "7.3.*",
"symfony/web-link": "7.3.*",
"symfony/yaml": "7.3.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"bump-after-update": true,
"sort-packages": true
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*",
"symfony/polyfill-php82": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd",
"importmap:install": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.3.*"
}
},
"require-dev": {
"phpunit/phpunit": "^12.3",
"symfony/browser-kit": "7.3.*",
"symfony/css-selector": "7.3.*",
"symfony/debug-bundle": "7.3.*",
"symfony/maker-bundle": "^1.0",
"symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.3.*"
}
}

9964
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

16
config/bundles.php Normal file
View file

@ -0,0 +1,16 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
];

View file

@ -0,0 +1,11 @@
framework:
asset_mapper:
# The paths to make available to the asset mapper.
paths:
- assets/
missing_import_mode: strict
when@prod:
framework:
asset_mapper:
missing_import_mode: warn

View file

@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null

11
config/packages/csrf.yaml Normal file
View file

@ -0,0 +1,11 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout

View file

@ -0,0 +1,5 @@
when@dev:
debug:
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
# See the "server:dump" command to start a new server.
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

View file

@ -0,0 +1,54 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
controller_resolver:
auto_mapping: false
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View file

@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false

View file

@ -0,0 +1,15 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
# Note that the session will be started ONLY if you read or write from it.
session: true
#esi: true
#fragments: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file

View file

@ -0,0 +1,3 @@
framework:
mailer:
dsn: '%env(MAILER_DSN)%'

View file

@ -0,0 +1,29 @@
framework:
messenger:
failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
retry_strategy:
max_retries: 3
multiplier: 2
failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
default_bus: messenger.bus.default
buses:
messenger.bus.default: []
routing:
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
Symfony\Component\Notifier\Message\ChatMessage: async
Symfony\Component\Notifier\Message\SmsMessage: async
# Route your messages to the transports
# 'App\Message\YourMessage': async

View file

@ -0,0 +1,62 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream
path: php://stderr
level: debug
formatter: monolog.formatter.json
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: stream
channels: [deprecation]
path: php://stderr
formatter: monolog.formatter.json

View file

@ -0,0 +1,12 @@
framework:
notifier:
chatter_transports:
texter_transports:
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }

View file

@ -0,0 +1,3 @@
framework:
property_info:
with_constructor_extractor: true

View file

@ -0,0 +1,10 @@
framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost
when@prod:
framework:
router:
strict_requirements: null

View file

@ -0,0 +1,39 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

View file

@ -0,0 +1,5 @@
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
providers:

View file

@ -0,0 +1,6 @@
twig:
file_name_pattern: '*.twig'
when@test:
twig:
strict_variables: true

View file

@ -0,0 +1,4 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
csrf_protection:
check_header: true

View file

@ -0,0 +1,11 @@
framework:
validation:
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
when@test:
framework:
validation:
not_compromised_password: false

View file

@ -0,0 +1,13 @@
when@dev:
web_profiler:
toolbar: true
framework:
profiler:
collect_serializer_data: true
when@test:
framework:
profiler:
collect: false
collect_serializer_data: true

5
config/preload.php Normal file
View file

@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

5
config/routes.yaml Normal file
View file

@ -0,0 +1,5 @@
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute

View file

@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error

View file

@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service

View file

@ -0,0 +1,8 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
prefix: /_profiler

20
config/services.yaml Normal file
View file

@ -0,0 +1,20 @@
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

28
importmap.php Normal file
View file

@ -0,0 +1,28 @@
<?php
/**
* Returns the importmap for this application.
*
* - "path" is a path inside the asset mapper system. Use the
* "debug:asset-map" command to see the full list of paths.
*
* - "entrypoint" (JavaScript only) set to true for any module that will
* be used as an "entrypoint" (and passed to the importmap() Twig function).
*
* The "importmap:require" command can be used to add new entries to this file.
*/
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'@hotwired/turbo' => [
'version' => '7.3.0',
],
];

0
migrations/.gitignore vendored Normal file
View file

5989
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

30
package.json Normal file
View file

@ -0,0 +1,30 @@
{
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
"@symfony/webpack-encore": "^5.0.0",
"chart.js": "^4.5.0",
"chartjs-plugin-datalabels": "^2.2.0",
"core-js": "^3.38.0",
"maplibre-gl": "^5.6.0",
"regenerator-runtime": "^0.13.9",
"table-sort-js": "^1.22.2",
"tablesort": "^5.6.0",
"webpack": "^5.74.0",
"webpack-cli": "^5.1.0"
},
"license": "UNLICENSED",
"private": true,
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
},
"dependencies": {
"charjs": "^0.0.1-security",
"chartjs-adapter-date-fns": "^3.0.0",
"jquery": "^3.7.1",
"table-sort": "^1.0.16"
}
}

44
phpunit.dist.xml Normal file
View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
failOnDeprecation="true"
failOnNotice="true"
failOnWarning="true"
bootstrap="tests/bootstrap.php"
cacheDirectory=".phpunit.cache"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source ignoreSuppressionOfDeprecations="true"
ignoreIndirectDeprecations="true"
restrictNotices="true"
restrictWarnings="true"
>
<include>
<directory>src</directory>
</include>
<deprecationTrigger>
<method>Doctrine\Deprecations\Deprecation::trigger</method>
<method>Doctrine\Deprecations\Deprecation::delegateTriggerToBackend</method>
<function>trigger_deprecation</function>
</deprecationTrigger>
</source>
<extensions>
</extensions>
</phpunit>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/assets/img/josm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

1
public/assets/js/main.js Normal file
View file

@ -0,0 +1 @@
console.log('Hello World');

6
public/css/bootstrap-icons.css vendored Normal file
View file

@ -0,0 +1,6 @@
@font-face {
font-display: block;
font-family: "bootstrap-icons";
src: url("../fonts/bootstrap-icons/bootstrap-icons.woff2") format("woff2"),
url("../fonts/bootstrap-icons/bootstrap-icons.woff") format("woff");
}

View file

@ -0,0 +1,92 @@
/* Styles pour la sidebar */
.city-sidebar {
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
padding: 1rem;
height: 100%;
position: fixed;
top: 0;
left: 0;
width: 100%;
overflow-y: auto;
z-index: 1000;
}
/* Desktop styles */
@media (min-width: 768px) {
.city-sidebar {
width: 25%;
max-width: 280px;
}
.main-content {
margin-left: 25%;
width: 75%;
padding-top: 20px;
}
.main-header {
margin-left: 25%;
width: 75%;
z-index: 1001;
}
.main-footer {
margin-left: 25%;
width: 75%;
}
}
/* Mobile styles */
@media (max-width: 767px) {
.city-sidebar {
position: relative;
width: 100%;
max-height: none;
}
.main-content {
margin-left: 0;
width: 100%;
}
.main-header {
margin-left: 0;
width: 100%;
}
.main-footer {
margin-left: 0;
width: 100%;
}
}
.city-sidebar .nav-link {
color: #495057;
border-radius: 0.25rem;
padding: 0.5rem 1rem;
margin-bottom: 0.25rem;
}
.city-sidebar .nav-link:hover {
background-color: #e9ecef;
}
.city-sidebar .nav-link.active {
background-color: #0d6efd;
color: white;
}
.city-sidebar .nav-link i {
width: 1.5rem;
text-align: center;
margin-right: 0.5rem;
}
.city-sidebar .sidebar-heading {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.1rem;
color: #6c757d;
margin-top: 1rem;
margin-bottom: 0.5rem;
padding-left: 0.5rem;
}
.section-anchor {
scroll-margin-top: 2rem;
}

328
public/css/global.css Normal file
View file

@ -0,0 +1,328 @@
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--accent-color: #e74c3c;
--text-color: #333;
--light-gray: #f5f6fa;
--border-color: #dcdde1;
--success-color: #27ae60;
--warning-color: #f1c40f;
--error-color: #c0392b;
}
/* Reset et styles de base */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--light-gray);
}
/* Layout */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
/* Navigation */
.navbar {
background-color: var(--primary-color);
padding: 1rem 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.navbar-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.navbar-brand {
color: white;
text-decoration: none;
font-size: 1.5rem;
font-weight: bold;
}
.navbar-menu {
display: flex;
gap: 1.5rem;
}
.navbar-link {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
.navbar-link:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* Cards */
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.card-title {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--primary-color);
}
/* Forms */
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: var(--secondary-color);
}
/* Buttons */
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
text-align: center;
text-decoration: none;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
}
.btn-primary {
background-color: var(--secondary-color);
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
transform: translateY(-1px);
}
.btn-secondary {
background-color: var(--primary-color);
color: white;
}
.btn-secondary:hover {
background-color: #2c3e50;
transform: translateY(-1px);
}
/* Alerts */
.alert {
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.alert-success {
background-color: #d4edda;
color: var(--success-color);
border: 1px solid #c3e6cb;
}
.alert-warning {
background-color: #fff3cd;
color: var(--warning-color);
border: 1px solid #ffeeba;
}
.alert-error {
background-color: #f8d7da;
color: var(--error-color);
border: 1px solid #f5c6cb;
}
/* Grid */
.grid {
display: grid;
gap: 1.5rem;
}
.grid-2 {
grid-template-columns: repeat(2, 1fr);
}
.grid-3 {
grid-template-columns: repeat(3, 1fr);
}
.grid-4 {
grid-template-columns: repeat(4, 1fr);
}
/* Responsive */
@media (max-width: 768px) {
.grid-2,
.grid-3,
.grid-4 {
grid-template-columns: 1fr;
}
.navbar-menu {
display: none;
}
.navbar-menu.active {
display: flex;
flex-direction: column;
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--primary-color);
padding: 1rem;
}
}
/* Utilities */
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
.mt-1 {
margin-top: 0.5rem;
}
.mt-2 {
margin-top: 1rem;
}
.mt-3 {
margin-top: 1.5rem;
}
.mt-4 {
margin-top: 2rem;
}
.mb-1 {
margin-bottom: 0.5rem;
}
.mb-2 {
margin-bottom: 1rem;
}
.mb-3 {
margin-bottom: 1.5rem;
}
.mb-4 {
margin-bottom: 2rem;
}
.p-1 {
padding: 0.5rem;
}
.p-2 {
padding: 1rem;
}
.p-3 {
padding: 1.5rem;
}
.p-4 {
padding: 2rem;
}
/* Map container */
.map-container {
width: 100%;
height: 400px;
border-radius: 8px;
overflow: hidden;
margin-bottom: 1.5rem;
}
/* Tags display */
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 1rem 0;
}
.tag {
background-color: var(--light-gray);
padding: 0.25rem 0.75rem;
border-radius: 16px;
font-size: 0.875rem;
color: var(--primary-color);
border: 1px solid var(--border-color);
}
/* Footer */
.footer {
background-color: var(--primary-color);
color: white;
padding: 2rem 0;
margin-top: 3rem;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-links {
display: flex;
gap: 1.5rem;
}
.footer-link {
color: white;
text-decoration: none;
opacity: 0.8;
transition: opacity 0.3s;
}
.footer-link:hover {
opacity: 1;
}

123
public/css/main.css Normal file
View file

@ -0,0 +1,123 @@
/* Layout général */
.body-landing {
background-color: rgb(255, 255, 255);
min-height: 100vh;
padding-bottom: 5rem;
}
/* Header */
.main-header {
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, .1);
padding: 1rem 0;
margin-bottom: 2rem;
}
.main-header h1 {
font-size: 1.5rem;
margin: 0;
}
.main-header a {
text-decoration: none;
color: inherit;
}
/* Footer */
.main-footer {
background-color: #f8f9fa;
padding: 2rem 0;
margin-top: 3rem;
border-top: 1px solid #dee2e6;
}
.main-footer p {
margin-bottom: 0.5rem;
}
.main-footer a {
color: #0d6efd;
text-decoration: none;
}
.main-footer a:hover {
text-decoration: underline;
}
/* Barre de progression */
#completion_progress {
position: fixed;
bottom: 0;
width: 100%;
padding: 1rem;
background-color: white;
border-top: 1px solid #e9ecef;
z-index: 1000;
box-shadow: 0 -2px 4px rgba(0, 0, 0, .1);
}
#completion_progress .container {
padding: 0;
}
/* Home page specific styles */
.hero-image {
max-width: 100%;
height: auto;
}
.feature-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
margin-bottom: 1rem;
font-size: 2rem;
color: #fff;
border-radius: 0.75rem;
background-color: #0d6efd;
}
.step-circle {
display: flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
border-radius: 50%;
background-color: #0d6efd;
color: white;
font-weight: bold;
font-size: 1.25rem;
margin-right: 1rem;
}
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important;
}
/* Media queries */
@media (max-width: 768px) {
.main-header h1 {
font-size: 1.25rem;
}
.main-footer {
padding: 1.5rem 0;
}
.display-4 {
font-size: 2.5rem;
}
.hero-image {
max-height: 200px;
margin-top: 2rem;
}
}

Binary file not shown.

Binary file not shown.

9
public/index.php Normal file
View file

@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

2
public/js/bootstrap/Sortable.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

6
public/js/bootstrap/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,780 @@
.mapboxgl-map {
-webkit-tap-highlight-color: rgb(0 0 0/0);
font: 12px/20px Helvetica Neue, Arial, Helvetica, sans-serif;
overflow: hidden;
position: relative
}
.mapboxgl-canvas {
left: 0;
position: absolute;
top: 0
}
.mapboxgl-map:-webkit-full-screen {
height: 100%;
width: 100%
}
.mapboxgl-canary {
background-color: salmon
}
.mapboxgl-canvas-container.mapboxgl-interactive,
.mapboxgl-ctrl-group button.mapboxgl-ctrl-compass {
cursor: grab;
-webkit-user-select: none;
user-select: none
}
.mapboxgl-canvas-container.mapboxgl-interactive.mapboxgl-track-pointer {
cursor: pointer
}
.mapboxgl-canvas-container.mapboxgl-interactive:active,
.mapboxgl-ctrl-group button.mapboxgl-ctrl-compass:active {
cursor: grabbing
}
.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate,
.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate .mapboxgl-canvas {
touch-action: pan-x pan-y
}
.mapboxgl-canvas-container.mapboxgl-touch-drag-pan,
.mapboxgl-canvas-container.mapboxgl-touch-drag-pan .mapboxgl-canvas {
touch-action: pinch-zoom
}
.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan,
.mapboxgl-canvas-container.mapboxgl-touch-zoom-rotate.mapboxgl-touch-drag-pan .mapboxgl-canvas {
touch-action: none
}
.mapboxgl-ctrl-bottom-left,
.mapboxgl-ctrl-bottom-right,
.mapboxgl-ctrl-top-left,
.mapboxgl-ctrl-top-right {
pointer-events: none;
position: absolute;
z-index: 2
}
.mapboxgl-ctrl-top-left {
left: 0;
top: 0
}
.mapboxgl-ctrl-top-right {
right: 0;
top: 0
}
.mapboxgl-ctrl-bottom-left {
bottom: 0;
left: 0
}
.mapboxgl-ctrl-bottom-right {
bottom: 0;
right: 0
}
.mapboxgl-ctrl {
clear: both;
pointer-events: auto;
transform: translate(0)
}
.mapboxgl-ctrl-top-left .mapboxgl-ctrl {
float: left;
margin: 10px 0 0 10px
}
.mapboxgl-ctrl-top-right .mapboxgl-ctrl {
float: right;
margin: 10px 10px 0 0
}
.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl {
float: left;
margin: 0 0 10px 10px
}
.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl {
float: right;
margin: 0 10px 10px 0
}
.mapboxgl-ctrl-group {
background: #fff;
border-radius: 4px
}
.mapboxgl-ctrl-group:not(:empty) {
box-shadow: 0 0 0 2px rgba(0, 0, 0, .1)
}
@media (-ms-high-contrast:active) {
.mapboxgl-ctrl-group:not(:empty) {
box-shadow: 0 0 0 2px ButtonText
}
}
.mapboxgl-ctrl-group button {
background-color: transparent;
border: 0;
box-sizing: border-box;
cursor: pointer;
display: block;
height: 29px;
outline: none;
overflow: hidden;
padding: 0;
width: 29px
}
.mapboxgl-ctrl-group button+button {
border-top: 1px solid #ddd
}
.mapboxgl-ctrl button .mapboxgl-ctrl-icon {
background-position: 50%;
background-repeat: no-repeat;
display: block;
height: 100%;
width: 100%
}
@media (-ms-high-contrast:active) {
.mapboxgl-ctrl-icon {
background-color: transparent
}
.mapboxgl-ctrl-group button+button {
border-top: 1px solid ButtonText
}
}
.mapboxgl-ctrl-attrib-button:focus,
.mapboxgl-ctrl-group button:focus {
box-shadow: 0 0 2px 2px #0096ff
}
.mapboxgl-ctrl button:disabled {
cursor: not-allowed
}
.mapboxgl-ctrl button:disabled .mapboxgl-ctrl-icon {
opacity: .25
}
.mapboxgl-ctrl-group button:first-child {
border-radius: 4px 4px 0 0
}
.mapboxgl-ctrl-group button:last-child {
border-radius: 0 0 4px 4px
}
.mapboxgl-ctrl-group button:only-child {
border-radius: inherit
}
.mapboxgl-ctrl button:not(:disabled):hover {
background-color: rgb(0 0 0/5%)
}
.mapboxgl-ctrl-group button:focus:focus-visible {
box-shadow: 0 0 2px 2px #0096ff
}
.mapboxgl-ctrl-group button:focus:not(:focus-visible) {
box-shadow: none
}
.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")
}
@media (-ms-high-contrast:active) {
.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")
}
}
@media (-ms-high-contrast:black-on-white) {
.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E")
}
}
.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")
}
@media (-ms-high-contrast:active) {
.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")
}
}
@media (-ms-high-contrast:black-on-white) {
.mapboxgl-ctrl button.mapboxgl-ctrl-fullscreen .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3h1zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16h1zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5H13zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1V7.5z'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-shrink .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1h-5.5zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1v-5.5zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1v5.5zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1h5.5z'/%3E%3C/svg%3E")
}
}
.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath id='south' d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")
}
@media (-ms-high-contrast:active) {
.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath id='south' d='M10.5 16l4 8 4-8h-8z' fill='%23999'/%3E%3C/svg%3E")
}
}
@media (-ms-high-contrast:black-on-white) {
.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23000' viewBox='0 0 29 29'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath id='south' d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")
}
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23aaa'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' fill='%23f00'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active-error .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e58978'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2' display='none'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background-error .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e54e33'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2' display='none'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-waiting .mapboxgl-ctrl-icon {
animation: mapboxgl-spin 2s linear infinite
}
@media (-ms-high-contrast:active) {
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23fff'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23999'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' fill='%23f00'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-active-error .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e58978'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%2333b5e5'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2' display='none'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate.mapboxgl-ctrl-geolocate-background-error .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23e54e33'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2' display='none'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
}
@media (-ms-high-contrast:black-on-white) {
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23000'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' display='none'/%3E%3C/svg%3E")
}
.mapboxgl-ctrl button.mapboxgl-ctrl-geolocate:disabled .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill='%23666'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1zm0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7z'/%3E%3Ccircle id='dot' cx='10' cy='10' r='2'/%3E%3Cpath id='stroke' d='M14 5l1 1-9 9-1-1 9-9z' fill='%23f00'/%3E%3C/svg%3E")
}
}
@keyframes mapboxgl-spin {
0% {
transform: rotate(0deg)
}
to {
transform: rotate(1turn)
}
}
a.mapboxgl-ctrl-logo {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd' viewBox='0 0 88 23'%3E%3Cdefs%3E%3Cpath id='logo' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='text' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='clip'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='white'/%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/mask%3E%3Cg id='outline' opacity='0.3' stroke='%23000' stroke-width='3'%3E%3Ccircle mask='url(%23clip)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23text' mask='url(%23clip)'/%3E%3C/g%3E%3Cg id='fill' opacity='0.9' fill='%23fff'%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/g%3E%3C/svg%3E");
background-repeat: no-repeat;
cursor: pointer;
display: block;
height: 23px;
margin: 0 0 -4px -4px;
overflow: hidden;
width: 88px
}
a.mapboxgl-ctrl-logo.mapboxgl-compact {
width: 23px
}
@media (-ms-high-contrast:active) {
a.mapboxgl-ctrl-logo {
background-color: transparent;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd' viewBox='0 0 88 23'%3E%3Cdefs%3E%3Cpath id='logo' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='text' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='clip'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='white'/%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/mask%3E%3Cg id='outline' opacity='1' stroke='%23000' stroke-width='3'%3E%3Ccircle mask='url(%23clip)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23text' mask='url(%23clip)'/%3E%3C/g%3E%3Cg id='fill' opacity='1' fill='%23fff'%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/g%3E%3C/svg%3E")
}
}
@media (-ms-high-contrast:black-on-white) {
a.mapboxgl-ctrl-logo {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' fill-rule='evenodd' viewBox='0 0 88 23'%3E%3Cdefs%3E%3Cpath id='logo' d='M11.5 2.25c5.105 0 9.25 4.145 9.25 9.25s-4.145 9.25-9.25 9.25-9.25-4.145-9.25-9.25 4.145-9.25 9.25-9.25zM6.997 15.983c-.051-.338-.828-5.802 2.233-8.873a4.395 4.395 0 013.13-1.28c1.27 0 2.49.51 3.39 1.42.91.9 1.42 2.12 1.42 3.39 0 1.18-.449 2.301-1.28 3.13C12.72 16.93 7 16 7 16l-.003-.017zM15.3 10.5l-2 .8-.8 2-.8-2-2-.8 2-.8.8-2 .8 2 2 .8z'/%3E%3Cpath id='text' d='M50.63 8c.13 0 .23.1.23.23V9c.7-.76 1.7-1.18 2.73-1.18 2.17 0 3.95 1.85 3.95 4.17s-1.77 4.19-3.94 4.19c-1.04 0-2.03-.43-2.74-1.18v3.77c0 .13-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V8.23c0-.12.1-.23.23-.23h1.4zm-3.86.01c.01 0 .01 0 .01-.01.13 0 .22.1.22.22v7.55c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V15c-.7.76-1.69 1.19-2.73 1.19-2.17 0-3.94-1.87-3.94-4.19 0-2.32 1.77-4.19 3.94-4.19 1.03 0 2.02.43 2.73 1.18v-.75c0-.12.1-.23.23-.23h1.4zm26.375-.19a4.24 4.24 0 00-4.16 3.29c-.13.59-.13 1.19 0 1.77a4.233 4.233 0 004.17 3.3c2.35 0 4.26-1.87 4.26-4.19 0-2.32-1.9-4.17-4.27-4.17zM60.63 5c.13 0 .23.1.23.23v3.76c.7-.76 1.7-1.18 2.73-1.18 1.88 0 3.45 1.4 3.84 3.28.13.59.13 1.2 0 1.8-.39 1.88-1.96 3.29-3.84 3.29-1.03 0-2.02-.43-2.73-1.18v.77c0 .12-.1.23-.23.23h-1.4c-.13 0-.23-.1-.23-.23V5.23c0-.12.1-.23.23-.23h1.4zm-34 11h-1.4c-.13 0-.23-.11-.23-.23V8.22c.01-.13.1-.22.23-.22h1.4c.13 0 .22.11.23.22v.68c.5-.68 1.3-1.09 2.16-1.1h.03c1.09 0 2.09.6 2.6 1.55.45-.95 1.4-1.55 2.44-1.56 1.62 0 2.93 1.25 2.9 2.78l.03 5.2c0 .13-.1.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.8 0-1.46.7-1.59 1.62l.01 4.68c0 .13-.11.23-.23.23h-1.41c-.13 0-.23-.11-.23-.23v-4.59c0-.98-.74-1.71-1.62-1.71-.85 0-1.54.79-1.6 1.8v4.5c0 .13-.1.23-.23.23zm53.615 0h-1.61c-.04 0-.08-.01-.12-.03-.09-.06-.13-.19-.06-.28l2.43-3.71-2.39-3.65a.213.213 0 01-.03-.12c0-.12.09-.21.21-.21h1.61c.13 0 .24.06.3.17l1.41 2.37 1.4-2.37a.34.34 0 01.3-.17h1.6c.04 0 .08.01.12.03.09.06.13.19.06.28l-2.37 3.65 2.43 3.7c0 .05.01.09.01.13 0 .12-.09.21-.21.21h-1.61c-.13 0-.24-.06-.3-.17l-1.44-2.42-1.44 2.42a.34.34 0 01-.3.17zm-7.12-1.49c-1.33 0-2.42-1.12-2.42-2.51 0-1.39 1.08-2.52 2.42-2.52 1.33 0 2.42 1.12 2.42 2.51 0 1.39-1.08 2.51-2.42 2.52zm-19.865 0c-1.32 0-2.39-1.11-2.42-2.48v-.07c.02-1.38 1.09-2.49 2.4-2.49 1.32 0 2.41 1.12 2.41 2.51 0 1.39-1.07 2.52-2.39 2.53zm-8.11-2.48c-.01 1.37-1.09 2.47-2.41 2.47s-2.42-1.12-2.42-2.51c0-1.39 1.08-2.52 2.4-2.52 1.33 0 2.39 1.11 2.41 2.48l.02.08zm18.12 2.47c-1.32 0-2.39-1.11-2.41-2.48v-.06c.02-1.38 1.09-2.48 2.41-2.48s2.42 1.12 2.42 2.51c0 1.39-1.09 2.51-2.42 2.51z'/%3E%3C/defs%3E%3Cmask id='clip'%3E%3Crect x='0' y='0' width='100%25' height='100%25' fill='white'/%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/mask%3E%3Cg id='outline' opacity='1' stroke='%23fff' stroke-width='3' fill='%23fff'%3E%3Ccircle mask='url(%23clip)' cx='11.5' cy='11.5' r='9.25'/%3E%3Cuse xlink:href='%23text' mask='url(%23clip)'/%3E%3C/g%3E%3Cg id='fill' opacity='1' fill='%23000'%3E%3Cuse xlink:href='%23logo'/%3E%3Cuse xlink:href='%23text'/%3E%3C/g%3E%3C/svg%3E")
}
}
.mapboxgl-ctrl.mapboxgl-ctrl-attrib {
background-color: hsla(0, 0%, 100%, .5);
margin: 0;
padding: 0 5px
}
@media screen {
.mapboxgl-ctrl-attrib.mapboxgl-compact {
background-color: #fff;
border-radius: 12px;
margin: 10px;
min-height: 20px;
padding: 2px 24px 2px 0;
position: relative
}
.mapboxgl-ctrl-attrib.mapboxgl-compact-show {
padding: 2px 28px 2px 8px;
visibility: visible
}
.mapboxgl-ctrl-bottom-left>.mapboxgl-ctrl-attrib.mapboxgl-compact-show,
.mapboxgl-ctrl-top-left>.mapboxgl-ctrl-attrib.mapboxgl-compact-show {
border-radius: 12px;
padding: 2px 8px 2px 28px
}
.mapboxgl-ctrl-attrib.mapboxgl-compact .mapboxgl-ctrl-attrib-inner {
display: none
}
.mapboxgl-ctrl-attrib-button {
background-color: hsla(0, 0%, 100%, .5);
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E");
border: 0;
border-radius: 12px;
box-sizing: border-box;
cursor: pointer;
display: none;
height: 24px;
outline: none;
position: absolute;
right: 0;
top: 0;
width: 24px
}
.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl-attrib-button,
.mapboxgl-ctrl-top-left .mapboxgl-ctrl-attrib-button {
left: 0
}
.mapboxgl-ctrl-attrib.mapboxgl-compact .mapboxgl-ctrl-attrib-button,
.mapboxgl-ctrl-attrib.mapboxgl-compact-show .mapboxgl-ctrl-attrib-inner {
display: block
}
.mapboxgl-ctrl-attrib.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button {
background-color: rgb(0 0 0/5%)
}
.mapboxgl-ctrl-bottom-right>.mapboxgl-ctrl-attrib.mapboxgl-compact:after {
bottom: 0;
right: 0
}
.mapboxgl-ctrl-top-right>.mapboxgl-ctrl-attrib.mapboxgl-compact:after {
right: 0;
top: 0
}
.mapboxgl-ctrl-top-left>.mapboxgl-ctrl-attrib.mapboxgl-compact:after {
left: 0;
top: 0
}
.mapboxgl-ctrl-bottom-left>.mapboxgl-ctrl-attrib.mapboxgl-compact:after {
bottom: 0;
left: 0
}
}
@media screen and (-ms-high-contrast:active) {
.mapboxgl-ctrl-attrib.mapboxgl-compact:after {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd' fill='%23fff'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")
}
}
@media screen and (-ms-high-contrast:black-on-white) {
.mapboxgl-ctrl-attrib.mapboxgl-compact:after {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg' fill-rule='evenodd'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")
}
}
.mapboxgl-ctrl-attrib a {
color: rgba(0, 0, 0, .75);
text-decoration: none
}
.mapboxgl-ctrl-attrib a:hover {
color: inherit;
text-decoration: underline
}
.mapboxgl-ctrl-attrib .mapbox-improve-map {
font-weight: 700;
margin-left: 2px
}
.mapboxgl-attrib-empty {
display: none
}
.mapboxgl-ctrl-scale {
background-color: hsla(0, 0%, 100%, .75);
border: 2px solid #333;
border-top: #333;
box-sizing: border-box;
color: #333;
font-size: 10px;
padding: 0 5px;
white-space: nowrap
}
.mapboxgl-popup {
display: flex;
left: 0;
pointer-events: none;
position: absolute;
top: 0;
will-change: transform
}
.mapboxgl-popup-anchor-top,
.mapboxgl-popup-anchor-top-left,
.mapboxgl-popup-anchor-top-right {
flex-direction: column
}
.mapboxgl-popup-anchor-bottom,
.mapboxgl-popup-anchor-bottom-left,
.mapboxgl-popup-anchor-bottom-right {
flex-direction: column-reverse
}
.mapboxgl-popup-anchor-left {
flex-direction: row
}
.mapboxgl-popup-anchor-right {
flex-direction: row-reverse
}
.mapboxgl-popup-tip {
border: 10px solid transparent;
height: 0;
width: 0;
z-index: 1
}
.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
align-self: center;
border-bottom-color: #fff;
border-top: none
}
.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip {
align-self: flex-start;
border-bottom-color: #fff;
border-left: none;
border-top: none
}
.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip {
align-self: flex-end;
border-bottom-color: #fff;
border-right: none;
border-top: none
}
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
align-self: center;
border-bottom: none;
border-top-color: #fff
}
.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip {
align-self: flex-start;
border-bottom: none;
border-left: none;
border-top-color: #fff
}
.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip {
align-self: flex-end;
border-bottom: none;
border-right: none;
border-top-color: #fff
}
.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
align-self: center;
border-left: none;
border-right-color: #fff
}
.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
align-self: center;
border-left-color: #fff;
border-right: none
}
.mapboxgl-popup-close-button {
background-color: transparent;
border: 0;
border-radius: 0 3px 0 0;
cursor: pointer;
position: absolute;
right: 0;
top: 0
}
.mapboxgl-popup-close-button:hover {
background-color: rgb(0 0 0/5%)
}
.mapboxgl-popup-content {
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
padding: 10px 10px 15px;
pointer-events: auto;
position: relative
}
.mapboxgl-popup-anchor-top-left .mapboxgl-popup-content {
border-top-left-radius: 0
}
.mapboxgl-popup-anchor-top-right .mapboxgl-popup-content {
border-top-right-radius: 0
}
.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-content {
border-bottom-left-radius: 0
}
.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-content {
border-bottom-right-radius: 0
}
.mapboxgl-popup-track-pointer {
display: none
}
.mapboxgl-popup-track-pointer * {
pointer-events: none;
user-select: none
}
.mapboxgl-map:hover .mapboxgl-popup-track-pointer {
display: flex
}
.mapboxgl-map:active .mapboxgl-popup-track-pointer {
display: none
}
.mapboxgl-marker {
left: 0;
opacity: 1;
position: absolute;
top: 0;
transition: opacity .2s;
will-change: transform
}
.mapboxgl-user-location-dot,
.mapboxgl-user-location-dot:before {
background-color: #1da1f2;
border-radius: 50%;
height: 15px;
width: 15px
}
.mapboxgl-user-location-dot:before {
animation: mapboxgl-user-location-dot-pulse 2s infinite;
content: "";
position: absolute
}
.mapboxgl-user-location-dot:after {
border: 2px solid #fff;
border-radius: 50%;
box-shadow: 0 0 3px rgba(0, 0, 0, .35);
box-sizing: border-box;
content: "";
height: 19px;
left: -2px;
position: absolute;
top: -2px;
width: 19px
}
.mapboxgl-user-location-show-heading .mapboxgl-user-location-heading {
height: 0;
width: 0
}
.mapboxgl-user-location-show-heading .mapboxgl-user-location-heading:after,
.mapboxgl-user-location-show-heading .mapboxgl-user-location-heading:before {
border-bottom: 7.5px solid #4aa1eb;
content: "";
position: absolute
}
.mapboxgl-user-location-show-heading .mapboxgl-user-location-heading:before {
border-left: 7.5px solid transparent;
transform: translateY(-28px) skewY(-20deg)
}
.mapboxgl-user-location-show-heading .mapboxgl-user-location-heading:after {
border-right: 7.5px solid transparent;
transform: translate(7.5px, -28px) skewY(20deg)
}
@keyframes mapboxgl-user-location-dot-pulse {
0% {
opacity: 1;
transform: scale(1)
}
70% {
opacity: 0;
transform: scale(3)
}
to {
opacity: 0;
transform: scale(1)
}
}
.mapboxgl-user-location-dot-stale {
background-color: #aaa
}
.mapboxgl-user-location-dot-stale:after {
display: none
}
.mapboxgl-user-location-accuracy-circle {
background-color: #1da1f233;
border-radius: 100%;
height: 1px;
width: 1px
}
.mapboxgl-crosshair,
.mapboxgl-crosshair .mapboxgl-interactive,
.mapboxgl-crosshair .mapboxgl-interactive:active {
cursor: crosshair
}
.mapboxgl-boxzoom {
background: #fff;
border: 2px dotted #202020;
height: 0;
left: 0;
opacity: .5;
position: absolute;
top: 0;
width: 0
}
@media print {
.mapbox-improve-map {
display: none
}
}
.mapboxgl-scroll-zoom-blocker,
.mapboxgl-touch-pan-blocker {
align-items: center;
background: rgba(0, 0, 0, .7);
color: #fff;
display: flex;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif;
height: 100%;
justify-content: center;
left: 0;
opacity: 0;
pointer-events: none;
position: absolute;
text-align: center;
top: 0;
transition: opacity .75s ease-in-out;
transition-delay: 1s;
width: 100%
}
.mapboxgl-scroll-zoom-blocker-show,
.mapboxgl-touch-pan-blocker-show {
opacity: 1;
transition: opacity .1s ease-in-out
}
.mapboxgl-canvas-container.mapboxgl-touch-pan-blocker-override.mapboxgl-scrollable-page,
.mapboxgl-canvas-container.mapboxgl-touch-pan-blocker-override.mapboxgl-scrollable-page .mapboxgl-canvas {
touch-action: pan-x pan-y
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
public/js/qrcode/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

92
public/js/turf/turf.min.js vendored Normal file

File diff suppressed because one or more lines are too long

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
beautifulsoup4==4.13.5
docutils==0.22
matplotlib==3.10.6
pandas==2.3.2
plotly==6.3.0

0
src/Controller/.gitignore vendored Normal file
View file

File diff suppressed because it is too large Load diff

0
src/Entity/.gitignore vendored Normal file
View file

Some files were not shown because too many files have changed in this diff Show more