├── data
├── bwapi
│ ├── __init__.py
│ ├── README.txt
│ └── server.py
├── food
│ └── __init__.py
├── common
│ ├── __init__.py
│ ├── delete_methods.py
│ ├── import_.py
│ ├── distances
│ │ └── json_to_csv.py
│ └── impacts.py
├── textile
│ ├── __init__.py
│ └── activities.py
├── .gitignore
├── setup.py
├── docker
│ ├── entrypoint.sh
│ ├── requirements.txt
│ └── Dockerfile
├── import_ecoinvent.py
├── import_method.py
└── README.md
├── Procfile
├── review
├── .gitignore
├── elm.json
└── src
│ └── ReviewConfig.elm
├── public
├── robots.txt
├── img
│ ├── chevron-down.png
│ ├── img_outil_api.png
│ ├── picto_bulle.png
│ ├── picto_textile.png
│ ├── illustration-score.png
│ ├── img_outil_methode.png
│ ├── ingredient-editor.png
│ ├── picto_alimentaire.png
│ ├── img_outil_calculateur.png
│ └── logo.svg
├── fonts
│ ├── Marianne-Bold.woff
│ ├── Marianne-Bold.woff2
│ ├── Marianne-Regular.woff
│ └── Marianne-Regular.woff2
├── icomoon
│ ├── fonts
│ │ ├── icomoon.eot
│ │ ├── icomoon.ttf
│ │ └── icomoon.woff
│ └── style.css
└── pages
│ ├── accessibilité.md
│ └── mentions-légales.md
├── tests
├── .gitignore
├── Data
│ ├── Food
│ │ ├── ProcessTest.elm
│ │ └── PreparationTest.elm
│ ├── GitbookTest.elm
│ ├── Textile
│ │ ├── ProductTest.elm
│ │ └── LifeCycleTest.elm
│ ├── TransportTest.elm
│ └── DefinitionTest.elm
├── TestUtils.elm
├── Views
│ ├── MarkdownTest.elm
│ └── FormatTest.elm
├── Request
│ └── VersionTest.elm
└── Server
│ └── ServerTest.elm
├── .prettierrc
├── .proxyrc.json
├── .env.sample
├── .prettierignore
├── bin
├── update-version.sh
└── build-db
├── .husky
├── pre-commit
└── _
│ └── husky.sh
├── nodemon.json
├── .editorconfig
├── src
├── Views
│ ├── Component
│ │ ├── StepsBorder.elm
│ │ ├── DownArrow.elm
│ │ ├── MassInput.elm
│ │ └── SplitInput.elm
│ ├── Debug.elm
│ ├── Container.elm
│ ├── Spinner.elm
│ ├── Link.elm
│ ├── Score.elm
│ ├── ComplementsDetails.elm
│ ├── CountrySelect.elm
│ ├── Button.elm
│ ├── CardTabs.elm
│ ├── Icon.elm
│ ├── Transport.elm
│ ├── Sidebar.elm
│ ├── AutocompleteSelector.elm
│ ├── Modal.elm
│ ├── Alert.elm
│ ├── Impact.elm
│ └── Table.elm
├── Page
│ ├── Explore
│ │ ├── Common.elm
│ │ ├── TextileProcesses.elm
│ │ ├── FoodProcesses.elm
│ │ └── Table.elm
│ └── Editorial.elm
├── Server
│ ├── Request.elm
│ └── Route.elm
├── Data
│ ├── Key.elm
│ ├── Zone.elm
│ ├── Color.elm
│ ├── Food
│ │ ├── Origin.elm
│ │ ├── Db.elm
│ │ └── Ingredient
│ │ │ └── Category.elm
│ ├── Github.elm
│ ├── Textile
│ │ ├── DyeingMedium.elm
│ │ ├── HeatSource.elm
│ │ ├── Db.elm
│ │ ├── MakingComplexity.elm
│ │ ├── Printing.elm
│ │ ├── Material
│ │ │ └── Origin.elm
│ │ └── Material.elm
│ ├── Env.elm
│ ├── Scope.elm
│ ├── Scoring.elm
│ ├── AutocompleteSelector.elm
│ ├── Matomo.elm
│ ├── Bookmark.elm
│ └── Split.elm
├── Request
│ ├── Common.elm
│ ├── Github.elm
│ ├── Matomo.elm
│ └── Version.elm
├── Ports.elm
└── Static
│ └── Db.elm-template
├── .gitignore
├── index.html
├── LICENSE
├── lib
├── charts
│ ├── stats.js
│ ├── base.js
│ ├── pefpie.js
│ ├── index.js
│ └── food-comparator.js
└── index.js
├── .github
└── workflows
│ ├── node.js.yml
│ └── codeql-analysis.yml
├── elm.json
├── package.json
├── index.js
└── server.js
/data/bwapi/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 |
--------------------------------------------------------------------------------
/data/food/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 |
--------------------------------------------------------------------------------
/data/common/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 |
--------------------------------------------------------------------------------
/data/textile/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm run server:start
2 |
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /*.csv
2 | /*.zip
3 |
4 |
--------------------------------------------------------------------------------
/review/.gitignore:
--------------------------------------------------------------------------------
1 | elm-stuff
2 | suppressed
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /branches/
3 |
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | e2e-food-output.json
2 | e2e-textile-output.json
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "trailingComma": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/.proxyrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "/api": {
3 | "target": "http://127.0.0.1:3000/"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/img/chevron-down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/img/chevron-down.png
--------------------------------------------------------------------------------
/public/img/img_outil_api.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/img/img_outil_api.png
--------------------------------------------------------------------------------
/public/img/picto_bulle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/img/picto_bulle.png
--------------------------------------------------------------------------------
/public/img/picto_textile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/img/picto_textile.png
--------------------------------------------------------------------------------
/public/fonts/Marianne-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/fonts/Marianne-Bold.woff
--------------------------------------------------------------------------------
/public/fonts/Marianne-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/fonts/Marianne-Bold.woff2
--------------------------------------------------------------------------------
/public/icomoon/fonts/icomoon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/icomoon/fonts/icomoon.eot
--------------------------------------------------------------------------------
/public/icomoon/fonts/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/icomoon/fonts/icomoon.ttf
--------------------------------------------------------------------------------
/public/icomoon/fonts/icomoon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/icomoon/fonts/icomoon.woff
--------------------------------------------------------------------------------
/public/img/illustration-score.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/img/illustration-score.png
--------------------------------------------------------------------------------
/public/img/img_outil_methode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/img/img_outil_methode.png
--------------------------------------------------------------------------------
/public/img/ingredient-editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/img/ingredient-editor.png
--------------------------------------------------------------------------------
/public/img/picto_alimentaire.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/img/picto_alimentaire.png
--------------------------------------------------------------------------------
/public/fonts/Marianne-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/fonts/Marianne-Regular.woff
--------------------------------------------------------------------------------
/public/fonts/Marianne-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/fonts/Marianne-Regular.woff2
--------------------------------------------------------------------------------
/public/img/img_outil_calculateur.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/n1k0/ecobalyse/master/public/img/img_outil_calculateur.png
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | MATOMO_HOST=stats.beta.gouv.fr
2 | MATOMO_SITE_ID=57
3 | MATOMO_TOKEN=xxx
4 | SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.yml
2 | *.yaml
3 | *.html
4 | package-lock.json
5 | public/icomoon/*
6 | public/vendor/*
7 | styles.scss
8 | tests/e2e-*-output.json
9 |
--------------------------------------------------------------------------------
/data/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name="ecobalyse_data",
5 | version="1.0",
6 | packages=["food", "common", "textile"],
7 | )
8 |
--------------------------------------------------------------------------------
/bin/update-version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # On scalingo we don't have a git repository, but we have a SOURCE_VERSION env var
4 | HASH=${SOURCE_VERSION:-`git rev-parse HEAD`}
5 |
6 | echo "{\"hash\": \"$HASH\"}" > public/version.json
7 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | # Check if code is formatted
5 | if ! npm run format:check ; then
6 | echo "Code is not formatted. Run 'npm run format:json' to format before committing."
7 | exit 1
8 | fi
9 |
--------------------------------------------------------------------------------
/data/common/delete_methods.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import bw2data
3 | import bw2io
4 |
5 | bw2data.projects.set_current("textile")
6 | for i in list(bw2data.methods):
7 | del bw2data.methods[i]
8 |
9 | # del bw2data.databases["biosphere3"]
10 | # bw2io.bw2setup()
11 |
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "NODE_ENV": "development"
4 | },
5 | "ext": "js,mjs,json,elm,yaml",
6 | "ignore": ["tests/", ".git", "node_modules/**/node_modules"],
7 | "watch": ["server.js", "server-app.js", "lib", "openapi.yaml", "public/data", "public/pages"]
8 | }
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.elm]
12 | indent_size = 4
13 |
14 | [elm.json]
15 | indent_size = 4
16 |
--------------------------------------------------------------------------------
/src/Views/Component/StepsBorder.elm:
--------------------------------------------------------------------------------
1 | module Views.Component.StepsBorder exposing (style)
2 |
3 | import Html
4 | import Html.Attributes as Attrs
5 |
6 |
7 | style : String -> Html.Attribute msg
8 | style color =
9 | Attrs.style "border-left" ("5px " ++ color ++ " solid")
10 |
--------------------------------------------------------------------------------
/src/Views/Debug.elm:
--------------------------------------------------------------------------------
1 | module Views.Debug exposing (view)
2 |
3 | import DebugToJson
4 | import Html exposing (..)
5 |
6 |
7 | view : List (Attribute msg) -> a -> Html msg
8 | view attrs =
9 | Debug.toString
10 | >> DebugToJson.pp
11 | >> text
12 | >> List.singleton
13 | >> pre attrs
14 |
--------------------------------------------------------------------------------
/data/docker/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ECOBALYSE_ID=$(ls -lnd /home/jovyan/ecobalyse|awk '{print $3}')
4 | JOVYAN_ID=$(id -u jovyan)
5 |
6 | if [ $ECOBALYSE_ID -ne $JOVYAN_ID ]; then
7 | usermod -u $ECOBALYSE_ID jovyan
8 | fi
9 |
10 | pushd /home/jovyan/${ECOBALYSE:=ecobalyse}/data
11 | pip install -e .
12 | popd
13 |
14 | ldconfig
15 |
16 | gosu jovyan "$@"
17 |
--------------------------------------------------------------------------------
/src/Views/Container.elm:
--------------------------------------------------------------------------------
1 | module Views.Container exposing
2 | ( centered
3 | , fluid
4 | )
5 |
6 | import Html exposing (..)
7 | import Html.Attributes exposing (..)
8 |
9 |
10 | centered : List (Attribute msg) -> List (Html msg) -> Html msg
11 | centered attrs =
12 | div (class "container" :: attrs)
13 |
14 |
15 | fluid : List (Attribute msg) -> List (Html msg) -> Html msg
16 | fluid attrs =
17 | div (class "container-fluid" :: attrs)
18 |
--------------------------------------------------------------------------------
/src/Views/Spinner.elm:
--------------------------------------------------------------------------------
1 | module Views.Spinner exposing (view)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 |
6 |
7 | view : Html msg
8 | view =
9 | div
10 | [ class "d-flex flex-column gap-3 justify-content-center align-items-center"
11 | , style "min-height" "25vh"
12 | ]
13 | [ div [ class "spinner-border text-primary", attribute "role" "status" ] []
14 | , p [ class "text-muted" ]
15 | [ text "Chargement…" ]
16 | ]
17 |
--------------------------------------------------------------------------------
/src/Page/Explore/Common.elm:
--------------------------------------------------------------------------------
1 | module Page.Explore.Common exposing (scopesView)
2 |
3 | import Data.Scope as Scope exposing (Scope)
4 | import Html exposing (..)
5 | import Html.Attributes exposing (..)
6 |
7 |
8 | scopesView : { a | scopes : List Scope } -> Html msg
9 | scopesView =
10 | .scopes
11 | >> List.map
12 | (\scope ->
13 | span [ class "badge badge-success" ]
14 | [ text <| Scope.toLabel scope ]
15 | )
16 | >> div [ class "d-flex gap-1" ]
17 |
--------------------------------------------------------------------------------
/src/Server/Request.elm:
--------------------------------------------------------------------------------
1 | module Server.Request exposing (Request)
2 |
3 | import Json.Encode as Encode
4 |
5 |
6 | type alias Request =
7 | -- Notes:
8 | -- - `method` is ExpressJS `method` string (HTTP verb: GET, POST, etc.)
9 | -- - `url` is ExpressJS' request `url` string
10 | -- - `body` is the JSON body; if no JSON body exist in the request, fallbacks to `{}`
11 | -- - `jsResponseHandler` is an ExpressJS response callback function
12 | { method : String
13 | , url : String
14 | , body : Encode.Value
15 | , jsResponseHandler : Encode.Value
16 | }
17 |
--------------------------------------------------------------------------------
/src/Data/Key.elm:
--------------------------------------------------------------------------------
1 | module Data.Key exposing (escape)
2 |
3 | import Json.Decode as Decode exposing (Decoder)
4 |
5 |
6 | escape : msg -> Decoder msg
7 | escape msg =
8 | succeedForKeyCode 27 msg
9 |
10 |
11 | forKeyCode : Int -> msg -> Int -> Decoder msg
12 | forKeyCode key msg keyCode =
13 | if keyCode == key then
14 | Decode.succeed msg
15 |
16 | else
17 | Decode.fail (String.fromInt keyCode)
18 |
19 |
20 | succeedForKeyCode : Int -> msg -> Decoder msg
21 | succeedForKeyCode key msg =
22 | Decode.field "keyCode" Decode.int
23 | |> Decode.andThen (forKeyCode key msg)
24 |
--------------------------------------------------------------------------------
/src/Request/Common.elm:
--------------------------------------------------------------------------------
1 | module Request.Common exposing (errorToString)
2 |
3 | import Http
4 |
5 |
6 | errorToString : Http.Error -> String
7 | errorToString error =
8 | case error of
9 | Http.BadUrl url ->
10 | "URL invalide: " ++ url
11 |
12 | Http.Timeout ->
13 | "Délai dépassé."
14 |
15 | Http.NetworkError ->
16 | "Erreur de communication réseau. Êtes-vous connecté ?"
17 |
18 | Http.BadStatus status_code ->
19 | "Erreur HTTP " ++ String.fromInt status_code
20 |
21 | Http.BadBody body ->
22 | "Échec de l'interprétation de la réponse HTTP: " ++ body
23 |
--------------------------------------------------------------------------------
/src/Views/Component/DownArrow.elm:
--------------------------------------------------------------------------------
1 | module Views.Component.DownArrow exposing (view)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 |
6 |
7 | view : List (Html msg) -> List (Html msg) -> Html msg
8 | view leftChildren rightChildren =
9 | div [ class "d-flex justify-content-between text-muted" ]
10 | [ span
11 | [ class "w-50 fs-7 py-4"
12 | , style "padding" ".5rem 1rem"
13 | ]
14 | leftChildren
15 | , div [ class "DownArrow" ] []
16 | , div
17 | [ class "w-50 fs-7 py-4"
18 | , style "padding" ".5rem 1rem"
19 | ]
20 | rightChildren
21 | ]
22 |
--------------------------------------------------------------------------------
/src/Views/Link.elm:
--------------------------------------------------------------------------------
1 | module Views.Link exposing (external, internal, smallPillExternal)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 |
6 |
7 | external : List (Attribute msg) -> List (Html msg) -> Html msg
8 | external attrs =
9 | a (attrs ++ [ target "_blank", class "link-external", rel "noopener noreferrer" ])
10 |
11 |
12 | internal : List (Attribute msg) -> List (Html msg) -> Html msg
13 | internal attrs =
14 | a attrs
15 |
16 |
17 | smallPillExternal : List (Attribute msg) -> List (Html msg) -> Html msg
18 | smallPillExternal attrs =
19 | a
20 | (target "_blank"
21 | :: rel "noopener noreferrer"
22 | :: class "btn btn-sm text-secondary text-decoration-none btn-link p-0 ms-1"
23 | :: attrs
24 | )
25 |
--------------------------------------------------------------------------------
/tests/Data/Food/ProcessTest.elm:
--------------------------------------------------------------------------------
1 | module Data.Food.ProcessTest exposing (..)
2 |
3 | import Data.Food.Process as Process
4 | import Expect
5 | import Test exposing (..)
6 | import TestUtils exposing (asTest, suiteWithDb)
7 |
8 |
9 | suite : Test
10 | suite =
11 | suiteWithDb "Data.Food.Process"
12 | (\{ foodDb } ->
13 | [ describe "findByCode"
14 | [ foodDb.processes
15 | |> Process.findByIdentifier (Process.codeFromString "AGRIBALU000000003102592")
16 | |> Result.map (.name >> Process.nameToString)
17 | |> Expect.equal (Ok "Carrot, conventional, national average, at farm gate {FR} U")
18 | |> asTest "should find a process by its identifier"
19 | ]
20 | ]
21 | )
22 |
--------------------------------------------------------------------------------
/src/Ports.elm:
--------------------------------------------------------------------------------
1 | port module Ports exposing
2 | ( addBodyClass
3 | , appStarted
4 | , copyToClipboard
5 | , loadRapidoc
6 | , removeBodyClass
7 | , saveStore
8 | , scrollIntoView
9 | , scrollTo
10 | , storeChanged
11 | )
12 |
13 | -- Outgoing
14 |
15 |
16 | port addBodyClass : String -> Cmd msg
17 |
18 |
19 | port appStarted : () -> Cmd msg
20 |
21 |
22 | port copyToClipboard : String -> Cmd msg
23 |
24 |
25 | port loadRapidoc : String -> Cmd msg
26 |
27 |
28 | port removeBodyClass : String -> Cmd msg
29 |
30 |
31 | port saveStore : String -> Cmd msg
32 |
33 |
34 | port scrollIntoView : String -> Cmd msg
35 |
36 |
37 | port scrollTo : { x : Float, y : Float } -> Cmd msg
38 |
39 |
40 |
41 | -- Incoming
42 |
43 |
44 | port storeChanged : (String -> msg) -> Sub msg
45 |
--------------------------------------------------------------------------------
/src/Request/Github.elm:
--------------------------------------------------------------------------------
1 | module Request.Github exposing (getChangelog)
2 |
3 | import Data.Env as Env
4 | import Data.Github as Github
5 | import Data.Session exposing (Session)
6 | import Json.Decode as Decode
7 | import RemoteData exposing (WebData)
8 | import RemoteData.Http exposing (defaultConfig)
9 |
10 |
11 | config : RemoteData.Http.Config
12 | config =
13 | -- drop ALL headers because Parcel's proxy messes with them
14 | -- see https://stackoverflow.com/a/47840149/330911
15 | { defaultConfig | headers = [] }
16 |
17 |
18 | getChangelog : Session -> (WebData (List Github.Commit) -> msg) -> Cmd msg
19 | getChangelog _ event =
20 | RemoteData.Http.getWithConfig config
21 | ("https://api.github.com/repos/" ++ Env.githubRepository ++ "/commits")
22 | event
23 | (Decode.list Github.decodeCommit)
24 |
--------------------------------------------------------------------------------
/src/Views/Score.elm:
--------------------------------------------------------------------------------
1 | module Views.Score exposing (view)
2 |
3 | import Data.Impact exposing (Impacts)
4 | import Data.Impact.Definition exposing (Definition)
5 | import Html exposing (..)
6 | import Html.Attributes exposing (..)
7 | import Mass exposing (Mass)
8 | import Views.Format as Format
9 |
10 |
11 | type alias Config =
12 | { impactDefinition : Definition
13 | , mass : Mass
14 | , score : Impacts
15 | }
16 |
17 |
18 | view : Config -> Html msg
19 | view { impactDefinition, mass, score } =
20 | div [ class "card bg-secondary shadow-sm" ]
21 | [ div [ class "card-body text-center text-nowrap text-white display-3 lh-1" ]
22 | [ Format.formatImpact impactDefinition score ]
23 | , div [ class "card-footer text-white text-center" ]
24 | [ text "Pour ", Format.kg mass ]
25 | ]
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.env
2 | /dist
3 | elm-stuff
4 | elm-stuff
5 | /node_modules
6 | /public/app.js
7 | /public/icomoon/*.txt
8 | /public/icomoon/*.zip
9 | /public/icomoon/demo-files
10 | /public/icomoon/demo-files
11 | /public/icomoon/demo.html
12 | /public/main.css
13 | /public/main.css.map
14 | /server-app.js
15 | /src/Static/Db.elm
16 | /xls
17 | /public/version.json
18 | /.parcel-cache/
19 |
20 | .vscode
21 | *.pyc
22 | .DS_Store
23 | processes-no-impacts.json
24 | /data.egg-info/*
25 | /data/food/import_agb/agribalyse3_no_param.CSV
26 | /data/food/import_agb/agribalyse3_no_param.CSV.zip
27 | /data/food/import_agb3.1/AGB31.bw2package
28 | /data/Dockerfile
29 | /data/ecobalyse_data.egg-info/
30 | /data/ECOINVENT3.9.1/*
31 | /data/AGB3.1.1.20230306.CSV
32 | /data/Environmental Footprint 3.1 (adapted) patch wtu.CSV
33 | /data/Environmental Footprint 3.1 (adapted).CSV
34 | /data/venv/
35 |
--------------------------------------------------------------------------------
/.husky/_/husky.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | if [ -z "$husky_skip_init" ]; then
3 | debug () {
4 | if [ "$HUSKY_DEBUG" = "1" ]; then
5 | echo "husky (debug) - $1"
6 | fi
7 | }
8 |
9 | readonly hook_name="$(basename -- "$0")"
10 | debug "starting $hook_name..."
11 |
12 | if [ "$HUSKY" = "0" ]; then
13 | debug "HUSKY env variable is set to 0, skipping hook"
14 | exit 0
15 | fi
16 |
17 | if [ -f ~/.huskyrc ]; then
18 | debug "sourcing ~/.huskyrc"
19 | . ~/.huskyrc
20 | fi
21 |
22 | readonly husky_skip_init=1
23 | export husky_skip_init
24 | sh -e "$0" "$@"
25 | exitCode="$?"
26 |
27 | if [ $exitCode != 0 ]; then
28 | echo "husky - $hook_name hook exited with code $exitCode (error)"
29 | fi
30 |
31 | if [ $exitCode = 127 ]; then
32 | echo "husky - command not found in PATH=$PATH"
33 | fi
34 |
35 | exit $exitCode
36 | fi
37 |
--------------------------------------------------------------------------------
/src/Static/Db.elm-template:
--------------------------------------------------------------------------------
1 | module Static.Db exposing (Db, db)
2 |
3 |
4 | import Data.Food.Db as FoodDb
5 | import Data.Textile.Db as TextileDb
6 |
7 |
8 | type alias Db =
9 | { foodDb : FoodDb.Db, textileDb : TextileDb.Db }
10 |
11 |
12 | textileDb : Result String TextileDb.Db
13 | textileDb =
14 | """%textileJson%""" |> TextileDb.buildFromJson
15 |
16 |
17 | foodProcessesJson : String
18 | foodProcessesJson =
19 | """%foodProcessesJson%"""
20 |
21 |
22 | foodIngredientsJson : String
23 | foodIngredientsJson =
24 | """%foodIngredientsJson%"""
25 |
26 |
27 | foodDb : Result String FoodDb.Db
28 | foodDb =
29 | textileDb
30 | |> Result.andThen
31 | (\textileDbParsed ->
32 | FoodDb.buildFromJson textileDbParsed foodProcessesJson foodIngredientsJson
33 | )
34 |
35 |
36 | db : Result String Db
37 | db =
38 | Result.map2 Db foodDb textileDb
39 |
--------------------------------------------------------------------------------
/data/docker/requirements.txt:
--------------------------------------------------------------------------------
1 | # don't forget to keep sync the api link in Dockerfile
2 | #jupyter_contrib_nbextensions @ git+https://github.com/ipython-contrib/jupyter_contrib_nbextensions
3 | #jupyter_nbextensions_configurator @ git+https://github.com/Jupyter-contrib/jupyter_nbextensions_configurator
4 | IPython>=8.16, <9
5 | fastapi
6 | fjson>=0.1, <0.2
7 | flatdict>=4.0, <4.1
8 | git+https://github.com/brightway-lca/brightway2-analyzer@0.11.7
9 | git+https://github.com/brightway-lca/brightway2-calc@2.0.DEV16
10 | git+https://github.com/brightway-lca/brightway2-data@4.0.DEV33
11 | git+https://github.com/brightway-lca/brightway2-parameters@1.1.0
12 | git+https://github.com/brightway-lca/bw_projects@v2.1.0
13 | git+https://github.com/ccomb/brightway2-io@ccomb-2
14 | ipywidgets>=8.1, <8.2
15 | jupyter-collaboration
16 | matplotlib>=3.7, <3.8
17 | numpy>=1.25, <1.26
18 | olca-ipc==0.0.12
19 | pandas>=2.0, <2.1
20 | scipy>=1.11, <1.12
21 | uvicorn
22 | xlrd>=2.0, <2.1
23 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ecobalyse
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/Views/ComplementsDetails.elm:
--------------------------------------------------------------------------------
1 | module Views.ComplementsDetails exposing (view)
2 |
3 | import Data.Impact as Impact
4 | import Html exposing (..)
5 | import Html.Attributes exposing (..)
6 | import Views.Format as Format
7 |
8 |
9 | type alias Config =
10 | { complementsImpacts : Impact.ComplementsImpacts
11 | }
12 |
13 |
14 | view : Config -> List (Html msg) -> Html msg
15 | view { complementsImpacts } detailedImpacts =
16 | details [ class "ComplementsDetails fs-7" ]
17 | (summary []
18 | [ div [ class "ComplementsTable d-flex justify-content-between w-100" ]
19 | [ span [ title "Cliquez pour plier/déplier" ] [ text "Compléments" ]
20 | , span [ class "text-muted text-end", title "Total des compléments" ]
21 | [ complementsImpacts
22 | |> Impact.getTotalComplementsImpacts
23 | |> Format.complement
24 | ]
25 | ]
26 | ]
27 | :: detailedImpacts
28 | )
29 |
--------------------------------------------------------------------------------
/src/Data/Zone.elm:
--------------------------------------------------------------------------------
1 | module Data.Zone exposing
2 | ( Zone(..)
3 | , decode
4 | )
5 |
6 | import Json.Decode as Decode exposing (Decoder)
7 | import Json.Decode.Extra as DE
8 |
9 |
10 | type Zone
11 | = Africa
12 | | Asia
13 | | Europe
14 | | MiddleEast
15 | | NorthAmerica
16 | | Oceania
17 | | SouthAmerica
18 |
19 |
20 | decode : Decoder Zone
21 | decode =
22 | Decode.string
23 | |> Decode.andThen (fromString >> DE.fromResult)
24 |
25 |
26 | fromString : String -> Result String Zone
27 | fromString string =
28 | case string of
29 | "Africa" ->
30 | Ok Africa
31 |
32 | "Asia" ->
33 | Ok Asia
34 |
35 | "Europe" ->
36 | Ok Europe
37 |
38 | "Middle_East" ->
39 | Ok MiddleEast
40 |
41 | "North_America" ->
42 | Ok NorthAmerica
43 |
44 | "Oceania" ->
45 | Ok Oceania
46 |
47 | "South_America" ->
48 | Ok SouthAmerica
49 |
50 | _ ->
51 | Err <| "Zone géographique inconnue : " ++ string
52 |
--------------------------------------------------------------------------------
/src/Request/Matomo.elm:
--------------------------------------------------------------------------------
1 | module Request.Matomo exposing (getApiStats, getWebStats)
2 |
3 | import Data.Matomo as Matomo
4 | import Data.Session exposing (Session)
5 | import Http
6 | import RemoteData exposing (WebData)
7 |
8 |
9 | getStats : String -> String -> String -> (WebData (List Matomo.Stat) -> msg) -> Cmd msg
10 | getStats host jsonKey qs event =
11 | Http.get
12 | { url = "https://" ++ host ++ "/" ++ qs
13 | , expect =
14 | Matomo.decodeStats jsonKey
15 | |> Http.expectJson (RemoteData.fromResult >> event)
16 | }
17 |
18 |
19 | getApiStats : Session -> (WebData (List Matomo.Stat) -> msg) -> Cmd msg
20 | getApiStats { matomo } =
21 | "?module=API&method=Goals.get&format=json&idGoal=1&period=day&date=last30&idSite="
22 | ++ matomo.siteId
23 | |> getStats matomo.host "nb_conversions"
24 |
25 |
26 | getWebStats : Session -> (WebData (List Matomo.Stat) -> msg) -> Cmd msg
27 | getWebStats { matomo } =
28 | "?module=API&method=VisitsSummary.get&format=json&period=day&date=last30&idSite="
29 | ++ matomo.siteId
30 | |> getStats matomo.host "nb_visits"
31 |
--------------------------------------------------------------------------------
/src/Data/Color.elm:
--------------------------------------------------------------------------------
1 | module Data.Color exposing (..)
2 |
3 | -- Extracted from elm-charts for convenience & reuse
4 | -- Typical chart sequence is purple, pink, blue, green, red, yellow, turquoise, orange, moss, brown
5 |
6 |
7 | pink : String
8 | pink =
9 | "#ea60df"
10 |
11 |
12 | purple : String
13 | purple =
14 | "#7b4dff"
15 |
16 |
17 | blue : String
18 | blue =
19 | "#12a5ed"
20 |
21 |
22 | moss : String
23 | moss =
24 | "#92b42c"
25 |
26 |
27 | green : String
28 | green =
29 | "#71c614"
30 |
31 |
32 | orange : String
33 | orange =
34 | "#ff8400"
35 |
36 |
37 | turquoise : String
38 | turquoise =
39 | "#22d2ba"
40 |
41 |
42 | red : String
43 | red =
44 | "#f5325b"
45 |
46 |
47 | darkYellow : String
48 | darkYellow =
49 | "#eabd39"
50 |
51 |
52 | darkBlue : String
53 | darkBlue =
54 | "#7345f6"
55 |
56 |
57 | coral : String
58 | coral =
59 | "#ea7369"
60 |
61 |
62 | magenta : String
63 | magenta =
64 | "#db4cb2"
65 |
66 |
67 | brown : String
68 | brown =
69 | "#871c1c"
70 |
71 |
72 | mint : String
73 | mint =
74 | "#6df0d2"
75 |
76 |
77 | yellow : String
78 | yellow =
79 | "#ffca00"
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Ministères de la Transition écologique & de la Cohésion des territoires
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/data/common/import_.py:
--------------------------------------------------------------------------------
1 | import bw2data
2 |
3 |
4 | def add_missing_substances(project, biosphere):
5 | """Two additional substances provided by ecoinvent and that seem to be in 3.9.2 but not in 3.9.1"""
6 | substances = {
7 | "a35f0a31-fe92-56db-b0ca-cc878d270fde": {
8 | "name": "Hydrogen cyanide",
9 | "synonyms": ["Hydrocyanic acid", "Formic anammonide", "Formonitrile"],
10 | "categories": ("air",),
11 | "unit": "kilogram",
12 | "CAS Number": "000074-90-8",
13 | "formula": "HCN",
14 | },
15 | "ea53a165-9f19-54f2-90a3-3e5c7a05051f": {
16 | "name": "N,N-Dimethylformamide",
17 | "synonyms": ["Formamide, N,N-dimethyl-", "Dimethyl formamide"],
18 | "categories": ("air",),
19 | "unit": "kilogram",
20 | "CAS Number": "000068-12-2",
21 | "formula": "C3H7NO",
22 | },
23 | }
24 | bw2data.projects.set_current(project)
25 | bio = bw2data.Database(biosphere)
26 | for code, activity in substances.items():
27 | if not [flow for flow in bio if flow["code"] == code]:
28 | bio.new_activity(code, **activity)
29 |
--------------------------------------------------------------------------------
/src/Views/CountrySelect.elm:
--------------------------------------------------------------------------------
1 | module Views.CountrySelect exposing (view)
2 |
3 | import Data.Country as Country exposing (Country)
4 | import Data.Scope as Scope exposing (Scope)
5 | import Html exposing (..)
6 | import Html.Attributes exposing (..)
7 | import Html.Events exposing (..)
8 |
9 |
10 | type alias Config msg =
11 | { attributes : List (Attribute msg)
12 | , countries : List Country
13 | , onSelect : Country.Code -> msg
14 | , scope : Scope
15 | , selectedCountry : Country.Code
16 | }
17 |
18 |
19 | view : Config msg -> Html msg
20 | view { attributes, countries, onSelect, scope, selectedCountry } =
21 | countries
22 | |> Scope.only scope
23 | |> List.sortBy .name
24 | |> List.map
25 | (\{ code, name } ->
26 | option
27 | [ selected (selectedCountry == code)
28 | , value <| Country.codeToString code
29 | ]
30 | [ text name ]
31 | )
32 | |> select
33 | (class
34 | "form-select"
35 | :: onInput (Country.codeFromString >> onSelect)
36 | :: attributes
37 | )
38 |
--------------------------------------------------------------------------------
/src/Views/Button.elm:
--------------------------------------------------------------------------------
1 | module Views.Button exposing
2 | ( docsPill
3 | , docsPillLink
4 | , pillClasses
5 | , smallPill
6 | , smallPillLink
7 | )
8 |
9 | import Html exposing (..)
10 | import Html.Attributes exposing (..)
11 |
12 |
13 | pillClasses : String
14 | pillClasses =
15 | "d-inline-flex align-items-center btn btn-sm gap-1 rounded-pill"
16 |
17 |
18 | smallPillClasses : String
19 | smallPillClasses =
20 | pillClasses ++ " text-secondary text-decoration-none btn-link p-0 ms-1"
21 |
22 |
23 | smallPill : List (Attribute msg) -> List (Html msg) -> Html msg
24 | smallPill attrs =
25 | button (class smallPillClasses :: attrs)
26 |
27 |
28 | smallPillLink : List (Attribute msg) -> List (Html msg) -> Html msg
29 | smallPillLink attrs =
30 | a (class smallPillClasses :: attrs)
31 |
32 |
33 | docsPillClasses : String
34 | docsPillClasses =
35 | pillClasses ++ " btn-primary fs-7 py-0"
36 |
37 |
38 | docsPill : List (Attribute msg) -> List (Html msg) -> Html msg
39 | docsPill attrs =
40 | button (class docsPillClasses :: attrs)
41 |
42 |
43 | docsPillLink : List (Attribute msg) -> List (Html msg) -> Html msg
44 | docsPillLink attrs =
45 | a (class docsPillClasses :: attrs)
46 |
--------------------------------------------------------------------------------
/tests/Data/Food/PreparationTest.elm:
--------------------------------------------------------------------------------
1 | module Data.Food.PreparationTest exposing (..)
2 |
3 | import Data.Food.Preparation as Preparation
4 | import Data.Impact as Impact
5 | import Data.Impact.Definition as Definition
6 | import Data.Split as Split
7 | import Data.Unit as Unit
8 | import Energy
9 | import Expect
10 | import Mass
11 | import Test exposing (..)
12 | import TestUtils exposing (asTest, suiteWithDb)
13 |
14 |
15 | suite : Test
16 | suite =
17 | suiteWithDb "Data.Food.Preparation"
18 | (\{ foodDb } ->
19 | [ describe "apply"
20 | [ { id = Preparation.Id "sample"
21 | , name = "Sample"
22 | , elec = ( Energy.kilowattHours 1, Split.half )
23 | , heat = ( Energy.megajoules 1, Split.half )
24 | , applyRawToCookedRatio = False
25 | }
26 | |> Preparation.apply foodDb (Mass.kilograms 1)
27 | |> Impact.getImpact Definition.Cch
28 | |> Unit.impactToFloat
29 | |> Expect.within (Expect.Absolute 0.001) 0.08
30 | |> asTest "compute impacts from applying a consumption preparation technique"
31 | ]
32 | ]
33 | )
34 |
--------------------------------------------------------------------------------
/tests/TestUtils.elm:
--------------------------------------------------------------------------------
1 | module TestUtils exposing
2 | ( asTest
3 | , expectImpactsEqual
4 | , suiteWithDb
5 | )
6 |
7 | import Data.Impact as Impact exposing (Impacts)
8 | import Data.Impact.Definition as Definition exposing (Base)
9 | import Data.Unit as Unit
10 | import Expect exposing (Expectation)
11 | import Static.Db as StaticDb
12 | import Test exposing (..)
13 |
14 |
15 | asTest : String -> Expectation -> Test
16 | asTest label =
17 | always >> test label
18 |
19 |
20 | suiteWithDb : String -> (StaticDb.Db -> List Test) -> Test
21 | suiteWithDb name suite =
22 | case StaticDb.db of
23 | Ok db ->
24 | describe name (suite db)
25 |
26 | Err error ->
27 | describe name
28 | [ test "should load static database" <|
29 | \_ -> Expect.fail <| "Couldn't parse static database: " ++ error
30 | ]
31 |
32 |
33 | expectImpactsEqual : Base (Float -> Expectation) -> Impacts -> Expectation
34 | expectImpactsEqual impacts subject =
35 | Definition.trigrams
36 | |> List.map
37 | (\trigram ->
38 | Impact.getImpact trigram >> Unit.impactToFloat >> Definition.get trigram impacts
39 | )
40 | |> (\expectations ->
41 | Expect.all expectations subject
42 | )
43 |
--------------------------------------------------------------------------------
/lib/charts/stats.js:
--------------------------------------------------------------------------------
1 | import BaseChart from "./base";
2 |
3 | function ucfirst(s) {
4 | return s.charAt(0).toUpperCase() + s.slice(1);
5 | }
6 |
7 | export default class extends BaseChart {
8 | constructor() {
9 | super();
10 | }
11 |
12 | static get observedAttributes() {
13 | return ["data", "heading", "height", "unit"];
14 | }
15 |
16 | get config() {
17 | return {
18 | chart: {
19 | type: "spline",
20 | },
21 | accessibility: {
22 | description: `Évolution de la fréquentation`,
23 | },
24 | subtitle: {
25 | text: "30 derniers jours",
26 | },
27 | xAxis: {
28 | type: "datetime",
29 | },
30 | series: [{ lineWidth: 4 }],
31 | };
32 | }
33 |
34 | attributeChanged(name, oldValue, newValue) {
35 | if (name === "data") {
36 | const data = JSON.parse(newValue);
37 | this.chart.series[0].setData(data);
38 | } else if (name === "heading") {
39 | this.chart.setTitle({ text: newValue });
40 | } else if (name === "height") {
41 | this.chart.setSize(null, parseInt(newValue, 10));
42 | } else if (name === "unit") {
43 | const unitTitle = ucfirst(newValue + "s");
44 | this.chart.yAxis[0].setTitle({ text: unitTitle });
45 | this.chart.series[0].setName(unitTitle);
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Views/CardTabs.elm:
--------------------------------------------------------------------------------
1 | module Views.CardTabs exposing (view)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 | import Html.Events exposing (..)
6 |
7 |
8 | type alias Tab msg =
9 | { label : String
10 | , active : Bool
11 | , onTabClick : msg
12 | }
13 |
14 |
15 | type alias Config msg =
16 | { tabs : List (Tab msg)
17 | , content : List (Html msg)
18 | }
19 |
20 |
21 | view : Config msg -> Html msg
22 | view { tabs, content } =
23 | div [ class "card shadow-sm" ]
24 | (div [ class "card-header px-0 pb-0 border-bottom-0" ]
25 | [ tabs
26 | |> List.map
27 | (\{ label, onTabClick, active } ->
28 | li [ class "TabsTab nav-item", classList [ ( "active", active ) ] ]
29 | [ button
30 | [ class "nav-link no-outline border-top-0"
31 | , classList [ ( "active", active ) ]
32 | , onClick onTabClick
33 | ]
34 | [ text label ]
35 | ]
36 | )
37 | |> ul [ class "Tabs nav nav-tabs nav-fill justify-content-end gap-2 px-2" ]
38 | ]
39 | :: content
40 | )
41 |
--------------------------------------------------------------------------------
/src/Data/Food/Origin.elm:
--------------------------------------------------------------------------------
1 | module Data.Food.Origin exposing
2 | ( Origin(..)
3 | , decode
4 | , toLabel
5 | )
6 |
7 | import Json.Decode as Decode exposing (Decoder)
8 | import Json.Decode.Extra as DE
9 |
10 |
11 | type Origin
12 | = France
13 | | EuropeAndMaghreb
14 | | OutOfEuropeAndMaghreb
15 | | OutOfEuropeAndMaghrebByPlane
16 |
17 |
18 | decode : Decoder Origin
19 | decode =
20 | Decode.string
21 | |> Decode.andThen (fromString >> DE.fromResult)
22 |
23 |
24 | fromString : String -> Result String Origin
25 | fromString string =
26 | case string of
27 | "France" ->
28 | Ok France
29 |
30 | "EuropeAndMaghreb" ->
31 | Ok EuropeAndMaghreb
32 |
33 | "OutOfEuropeAndMaghreb" ->
34 | Ok OutOfEuropeAndMaghreb
35 |
36 | "OutOfEuropeAndMaghrebByPlane" ->
37 | Ok OutOfEuropeAndMaghrebByPlane
38 |
39 | _ ->
40 | Err <| "Origine géographique inconnue : " ++ string
41 |
42 |
43 | toLabel : Origin -> String
44 | toLabel origin =
45 | case origin of
46 | France ->
47 | "France"
48 |
49 | EuropeAndMaghreb ->
50 | "Europe et Maghreb"
51 |
52 | OutOfEuropeAndMaghreb ->
53 | "Hors Europe et Maghreb"
54 |
55 | OutOfEuropeAndMaghrebByPlane ->
56 | "Hors Europe et Maghreb par avion"
57 |
--------------------------------------------------------------------------------
/src/Data/Github.elm:
--------------------------------------------------------------------------------
1 | module Data.Github exposing (Commit, decodeCommit)
2 |
3 | import Iso8601
4 | import Json.Decode as Decode exposing (Decoder)
5 | import Json.Decode.Pipeline as Pipe
6 | import Time exposing (Posix)
7 |
8 |
9 | type alias Commit =
10 | { sha : String
11 | , message : String
12 | , date : Posix
13 | , authorName : String
14 | , authorLogin : String
15 | , authorAvatar : Maybe String
16 | }
17 |
18 |
19 | decodeCommit : Decoder Commit
20 | decodeCommit =
21 | Decode.succeed Commit
22 | |> Pipe.requiredAt [ "sha" ] Decode.string
23 | |> Pipe.requiredAt [ "commit", "message" ] Decode.string
24 | |> Pipe.requiredAt [ "commit", "author", "date" ] Iso8601.decoder
25 | |> Pipe.requiredAt [ "commit", "author", "name" ] Decode.string
26 | |> Pipe.optionalAt [ "author", "login" ] Decode.string "Ecobalyse"
27 | |> Pipe.optionalAt [ "author", "avatar_url" ] (Decode.maybe Decode.string) Nothing
28 | |> Decode.andThen
29 | (\({ authorAvatar, authorName } as commit) ->
30 | Decode.succeed
31 | (if authorAvatar == Nothing && authorName == "Ingredient editor" then
32 | { commit | authorAvatar = Just "img/ingredient-editor.png" }
33 |
34 | else
35 | commit
36 | )
37 | )
38 |
--------------------------------------------------------------------------------
/public/img/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Views/Component/MassInput.elm:
--------------------------------------------------------------------------------
1 | module Views.Component.MassInput exposing (view)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes as Attr exposing (..)
5 | import Html.Events exposing (..)
6 | import Mass exposing (Mass)
7 |
8 |
9 | type alias Config msg =
10 | { disabled : Bool
11 | , mass : Mass
12 | , onChange : Maybe Mass -> msg
13 | }
14 |
15 |
16 | view : Config msg -> Html msg
17 | view { disabled, mass, onChange } =
18 | div [ class "input-group input-group" ]
19 | [ input
20 | [ class "form-control text-end incdec-arrows-left"
21 | , type_ "number"
22 | , step "1"
23 | , mass
24 | |> Mass.inGrams
25 | |> round
26 | |> String.fromInt
27 | |> value
28 | , title "Quantité en grammes"
29 | , onInput <|
30 | \str ->
31 | onChange
32 | (if str == "" then
33 | Nothing
34 |
35 | else
36 | str
37 | |> String.toFloat
38 | |> Maybe.map Mass.grams
39 | )
40 | , Attr.min "0"
41 | , Attr.disabled disabled
42 | ]
43 | []
44 | , span [ class "input-group-text", title "grammes" ]
45 | [ text "g"
46 | ]
47 | ]
48 |
--------------------------------------------------------------------------------
/src/Views/Component/SplitInput.elm:
--------------------------------------------------------------------------------
1 | module Views.Component.SplitInput exposing (view)
2 |
3 | import Data.Split as Split exposing (Split)
4 | import Html exposing (..)
5 | import Html.Attributes as Attr exposing (..)
6 | import Html.Events exposing (..)
7 |
8 |
9 | type alias Config msg =
10 | { disabled : Bool
11 | , share : Split
12 | , onChange : Maybe Split -> msg
13 | }
14 |
15 |
16 | view : Config msg -> Html msg
17 | view { disabled, share, onChange } =
18 | div [ class "input-group input-group" ]
19 | [ input
20 | [ class "form-control text-end incdec-arrows-left"
21 | , type_ "number"
22 | , step "1"
23 | , share
24 | |> Split.toPercent
25 | |> String.fromInt
26 | |> value
27 | , title "Quantité en pourcents"
28 | , onInput <|
29 | \str ->
30 | onChange
31 | (if str == "" then
32 | Nothing
33 |
34 | else
35 | str
36 | |> String.toInt
37 | |> Maybe.andThen (Split.fromPercent >> Result.toMaybe)
38 | )
39 | , Attr.min "0"
40 | , Attr.disabled disabled
41 | ]
42 | []
43 | , span [ class "input-group-text", title "pourcents" ]
44 | [ text "%"
45 | ]
46 | ]
47 |
--------------------------------------------------------------------------------
/src/Data/Textile/DyeingMedium.elm:
--------------------------------------------------------------------------------
1 | module Data.Textile.DyeingMedium exposing
2 | ( DyeingMedium(..)
3 | , decode
4 | , encode
5 | , fromString
6 | , toLabel
7 | , toString
8 | )
9 |
10 | import Json.Decode as Decode exposing (Decoder)
11 | import Json.Decode.Extra as DE
12 | import Json.Encode as Encode
13 |
14 |
15 | type DyeingMedium
16 | = Article
17 | | Fabric
18 | | Yarn
19 |
20 |
21 | decode : Decoder DyeingMedium
22 | decode =
23 | Decode.string
24 | |> Decode.andThen (fromString >> DE.fromResult)
25 |
26 |
27 | encode : DyeingMedium -> Encode.Value
28 | encode =
29 | toString >> Encode.string
30 |
31 |
32 | fromString : String -> Result String DyeingMedium
33 | fromString string =
34 | case string of
35 | "article" ->
36 | Ok Article
37 |
38 | "fabric" ->
39 | Ok Fabric
40 |
41 | "yarn" ->
42 | Ok Yarn
43 |
44 | _ ->
45 | Err <| "Type de support de teinture inconnu: " ++ string
46 |
47 |
48 | toLabel : DyeingMedium -> String
49 | toLabel medium =
50 | case medium of
51 | Article ->
52 | "Article"
53 |
54 | Fabric ->
55 | "Tissu"
56 |
57 | Yarn ->
58 | "Fil"
59 |
60 |
61 | toString : DyeingMedium -> String
62 | toString medium =
63 | case medium of
64 | Article ->
65 | "article"
66 |
67 | Fabric ->
68 | "fabric"
69 |
70 | Yarn ->
71 | "yarn"
72 |
--------------------------------------------------------------------------------
/review/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": ["src"],
4 | "elm-version": "0.19.1",
5 | "dependencies": {
6 | "direct": {
7 | "elm/core": "1.0.5",
8 | "jfmengels/elm-review": "2.13.0",
9 | "jfmengels/elm-review-cognitive-complexity": "1.0.2",
10 | "jfmengels/elm-review-common": "1.3.2",
11 | "jfmengels/elm-review-debug": "1.0.8",
12 | "jfmengels/elm-review-simplify": "2.0.23",
13 | "jfmengels/elm-review-unused": "1.1.29",
14 | "stil4m/elm-syntax": "7.2.9",
15 | "truqu/elm-review-noredundantconcat": "1.0.1",
16 | "truqu/elm-review-noredundantcons": "1.0.1"
17 | },
18 | "indirect": {
19 | "elm/bytes": "1.0.8",
20 | "elm/html": "1.0.0",
21 | "elm/json": "1.1.3",
22 | "elm/parser": "1.1.0",
23 | "elm/project-metadata-utils": "1.0.2",
24 | "elm/random": "1.0.0",
25 | "elm/time": "1.0.0",
26 | "elm/virtual-dom": "1.0.3",
27 | "elm-community/list-extra": "8.7.0",
28 | "elm-explorations/test": "2.1.0",
29 | "miniBill/elm-unicode": "1.0.2",
30 | "pzp1997/assoc-list": "1.0.0",
31 | "rtfeldman/elm-hex": "1.0.0",
32 | "stil4m/structured-writer": "1.0.3"
33 | }
34 | },
35 | "test-dependencies": {
36 | "direct": {
37 | "elm-explorations/test": "2.1.0"
38 | },
39 | "indirect": {}
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Data/Env.elm:
--------------------------------------------------------------------------------
1 | module Data.Env exposing
2 | ( betagouvUrl
3 | , communityUrl
4 | , contactEmail
5 | , defaultDeadStock
6 | , gitbookUrl
7 | , githubRepository
8 | , githubUrl
9 | , maxMakingDeadStockRatio
10 | , maxMakingWasteRatio
11 | , maxMaterials
12 | , minMakingDeadStockRatio
13 | , minMakingWasteRatio
14 | )
15 |
16 | import Data.Split as Split exposing (Split)
17 |
18 |
19 | betagouvUrl : String
20 | betagouvUrl =
21 | "https://beta.gouv.fr/startups/ecobalyse.html"
22 |
23 |
24 | communityUrl : String
25 | communityUrl =
26 | "https://fabrique-numerique.gitbook.io/ecobalyse/communaute"
27 |
28 |
29 | contactEmail : String
30 | contactEmail =
31 | "ecobalyse@beta.gouv.fr"
32 |
33 |
34 | gitbookUrl : String
35 | gitbookUrl =
36 | "https://fabrique-numerique.gitbook.io/ecobalyse"
37 |
38 |
39 | githubRepository : String
40 | githubRepository =
41 | "MTES-MCT/ecobalyse"
42 |
43 |
44 | githubUrl : String
45 | githubUrl =
46 | "https://github.com/" ++ githubRepository
47 |
48 |
49 | minMakingWasteRatio : Split
50 | minMakingWasteRatio =
51 | Split.zero
52 |
53 |
54 | maxMakingWasteRatio : Split
55 | maxMakingWasteRatio =
56 | Split.fourty
57 |
58 |
59 | minMakingDeadStockRatio : Split
60 | minMakingDeadStockRatio =
61 | Split.zero
62 |
63 |
64 | maxMakingDeadStockRatio : Split
65 | maxMakingDeadStockRatio =
66 | Split.thirty
67 |
68 |
69 | defaultDeadStock : Split
70 | defaultDeadStock =
71 | Split.fifteen
72 |
73 |
74 | maxMaterials : Int
75 | maxMaterials =
76 | 5
77 |
--------------------------------------------------------------------------------
/lib/charts/base.js:
--------------------------------------------------------------------------------
1 | import Highcharts from "highcharts";
2 | import exportingOption from "highcharts/modules/exporting";
3 | import dataExportOption from "highcharts/modules/export-data";
4 | import offlineOption from "highcharts/modules/offline-exporting";
5 |
6 | exportingOption(Highcharts);
7 | dataExportOption(Highcharts);
8 | offlineOption(Highcharts);
9 |
10 | export default class extends HTMLElement {
11 | constructor() {
12 | super();
13 | }
14 |
15 | static get observedAttributes() {
16 | throw new Error("Must be implemented");
17 | }
18 |
19 | get config() {
20 | throw new Error("Must be implemented");
21 | }
22 |
23 | attributeChanged(name, oldValue, newValue) {
24 | throw new Error("Must be implemented");
25 | }
26 |
27 | get container() {
28 | return this.querySelector(".chart-container");
29 | }
30 |
31 | attributeChangedCallback(name, oldValue, newValue) {
32 | requestAnimationFrame(() => {
33 | this.attributeChanged(name, oldValue, newValue);
34 | });
35 | }
36 |
37 | connectedCallback() {
38 | this.appendChild(
39 | document.createRange().createContextualFragment(`
40 |
41 | `),
42 | );
43 |
44 | this.chart = Highcharts.chart(this.container, this.config);
45 |
46 | // Force reflow
47 | requestAnimationFrame(() => {
48 | this.chart.reflow();
49 | });
50 | }
51 |
52 | disconnectedCallback() {
53 | if (this.chart) {
54 | this.removeChild(this.container);
55 | this.chart.destroy();
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Request/Version.elm:
--------------------------------------------------------------------------------
1 | module Request.Version exposing
2 | ( Version(..)
3 | , loadVersion
4 | , pollVersion
5 | , toString
6 | , updateVersion
7 | )
8 |
9 | import Json.Decode as Decode
10 | import RemoteData exposing (WebData)
11 | import RemoteData.Http as Http
12 | import Time
13 |
14 |
15 | type Version
16 | = Unknown
17 | | Version String
18 | | NewerVersion
19 |
20 |
21 | toString : Version -> Maybe String
22 | toString version =
23 | case version of
24 | Version string ->
25 | Just string
26 |
27 | _ ->
28 | Nothing
29 |
30 |
31 | updateVersion : Version -> WebData String -> Version
32 | updateVersion currentVersion webData =
33 | case webData of
34 | RemoteData.Success v ->
35 | case currentVersion of
36 | Version currentV ->
37 | if currentV /= v then
38 | NewerVersion
39 |
40 | else
41 | currentVersion
42 |
43 | NewerVersion ->
44 | currentVersion
45 |
46 | _ ->
47 | Version v
48 |
49 | _ ->
50 | currentVersion
51 |
52 |
53 | hashDecoder : Decode.Decoder String
54 | hashDecoder =
55 | Decode.field "hash" Decode.string
56 |
57 |
58 | loadVersion : (WebData String -> msg) -> Cmd msg
59 | loadVersion event =
60 | Http.get "/version.json" event hashDecoder
61 |
62 |
63 | pollVersion : msg -> Sub msg
64 | pollVersion event =
65 | Time.every (60 * 1000) (always event)
66 |
--------------------------------------------------------------------------------
/tests/Data/GitbookTest.elm:
--------------------------------------------------------------------------------
1 | module Data.GitbookTest exposing (..)
2 |
3 | import Data.Env as Env
4 | import Data.Gitbook as Gitbook
5 | import Expect
6 | import Test exposing (..)
7 | import TestUtils exposing (asTest)
8 |
9 |
10 | suite : Test
11 | suite =
12 | describe "Data.Gitbook"
13 | [ describe "handleMarkdownLink"
14 | [ Gitbook.handleMarkdownGitbookLink Nothing "http://google.com"
15 | |> Expect.equal "http://google.com"
16 | |> asTest "should resolve an external link"
17 | , Gitbook.handleMarkdownGitbookLink (Just Gitbook.TextileSpinning) "http://google.com"
18 | |> Expect.equal "http://google.com"
19 | |> asTest "should resolve an external link even with a path provided"
20 | , Gitbook.handleMarkdownGitbookLink (Just Gitbook.TextileSpinning) "filature.md"
21 | |> Expect.equal (Env.gitbookUrl ++ "/textile/filature")
22 | |> asTest "should resolve an internal link from current page path"
23 | , Gitbook.handleMarkdownGitbookLink (Just Gitbook.TextileSpinning) "../faq.md"
24 | |> Expect.equal (Env.gitbookUrl ++ "/textile/../faq")
25 | |> asTest "should resolve an internal link from current page path down a folder level"
26 | , Gitbook.handleMarkdownGitbookLink (Just Gitbook.TextileSpinning) "foo/bar.md"
27 | |> Expect.equal (Env.gitbookUrl ++ "/textile/foo/bar")
28 | |> asTest "should resolve an internal link from current page path up a folder level"
29 | ]
30 | ]
31 |
--------------------------------------------------------------------------------
/lib/charts/pefpie.js:
--------------------------------------------------------------------------------
1 | import BaseChart from "./base";
2 |
3 | export default class extends BaseChart {
4 | constructor() {
5 | super();
6 | }
7 |
8 | static get observedAttributes() {
9 | return ["data"];
10 | }
11 |
12 | get config() {
13 | return {
14 | chart: {
15 | type: "pie",
16 | },
17 | title: null,
18 | caption: {
19 | text: `Le score PEF est calculé selon la méthodologie proposée par le PEFCR Apparel
20 | & Footwear. Dans un premier temps et faute de données disponibles dans la Base
21 | Impacts, l'épuisement des ressources en eau, l'ecotoxicité eau douce, la toxicité
22 | humaine (cancer) et la toxicité humaine (non cancer) ne sont pas pris en compte à ce
23 | stade.`,
24 | },
25 | plotOptions: {
26 | series: {
27 | animation: false,
28 | },
29 | pie: {
30 | allowPointSelect: true,
31 | cursor: "pointer",
32 | dataLabels: {
33 | enabled: true,
34 | format: "{point.name}: {point.percentage:.1f}%",
35 | },
36 | },
37 | },
38 | tooltip: {
39 | pointFormat: "{point.y:.2f} mPt ({point.percentage:.1f} %)",
40 | },
41 | series: [
42 | {
43 | name: "Valeur (mPt)",
44 | data: [],
45 | },
46 | ],
47 | };
48 | }
49 |
50 | attributeChanged(name, oldValue, newValue) {
51 | if (name === "data") {
52 | const data = JSON.parse(newValue);
53 | this.chart.series[0].setData(data);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Data/Scope.elm:
--------------------------------------------------------------------------------
1 | module Data.Scope exposing
2 | ( Scope(..)
3 | , decode
4 | , encode
5 | , only
6 | , parseSlug
7 | , toLabel
8 | , toString
9 | )
10 |
11 | import Json.Decode as Decode exposing (Decoder)
12 | import Json.Decode.Extra as DE
13 | import Json.Encode as Encode
14 | import Url.Parser as Parser exposing (Parser)
15 |
16 |
17 | type Scope
18 | = Food
19 | | Textile
20 |
21 |
22 | decode : Decoder Scope
23 | decode =
24 | Decode.string
25 | |> Decode.andThen (fromString >> DE.fromResult)
26 |
27 |
28 | encode : Scope -> Encode.Value
29 | encode =
30 | toString >> Encode.string
31 |
32 |
33 | fromString : String -> Result String Scope
34 | fromString string =
35 | case string of
36 | "textile" ->
37 | Ok Textile
38 |
39 | "food" ->
40 | Ok Food
41 |
42 | _ ->
43 | Err <| "Couldn't decode unknown scope " ++ string
44 |
45 |
46 | only : Scope -> List { a | scopes : List Scope } -> List { a | scopes : List Scope }
47 | only scope =
48 | List.filter (.scopes >> List.member scope)
49 |
50 |
51 | parseSlug : Parser (Scope -> a) a
52 | parseSlug =
53 | Parser.custom "SCOPE" <|
54 | (fromString >> Result.toMaybe)
55 |
56 |
57 | toLabel : Scope -> String
58 | toLabel scope =
59 | case scope of
60 | Food ->
61 | "Alimentaire"
62 |
63 | Textile ->
64 | "Textile"
65 |
66 |
67 | toString : Scope -> String
68 | toString scope =
69 | case scope of
70 | Food ->
71 | "food"
72 |
73 | Textile ->
74 | "textile"
75 |
--------------------------------------------------------------------------------
/tests/Views/MarkdownTest.elm:
--------------------------------------------------------------------------------
1 | module Views.MarkdownTest exposing (..)
2 |
3 | import Data.Gitbook as Gitbook
4 | import Expect
5 | import Html exposing (..)
6 | import Test exposing (..)
7 | import TestUtils exposing (asTest)
8 | import Views.Markdown as Markdown
9 |
10 |
11 | gitbookPage : String -> Gitbook.Page
12 | gitbookPage md =
13 | { title = ""
14 | , description = Nothing
15 | , path = Gitbook.TextileUse
16 | , markdown = md
17 | }
18 |
19 |
20 | suite : Test
21 | suite =
22 | describe "Views.Markdown"
23 | [ describe "Markdown.parse"
24 | -- NOTE: unfortunately, failing test results will show in diffs,
25 | -- making it super hard to debug. I couldn't identify any solution to this,
26 | -- yet it's important to have this test ensuring the very basics work.
27 | [ "plop"
28 | |> Markdown.Simple
29 | |> Markdown.parse
30 | |> Expect.equal (Ok [ p [] [ text "plop" ] ])
31 | |> asTest "should parse the simplest Markdown string"
32 | , gitbookPage "plop"
33 | |> Markdown.Gitbook
34 | |> Markdown.parse
35 | |> Expect.equal (Ok [ p [] [ text "plop" ] ])
36 | |> asTest "should parse the simplest Gitbook page Markdown string"
37 | , gitbookPage "Foo & Bar"
38 | |> Markdown.Gitbook
39 | |> Markdown.parse
40 | |> Expect.equal (Ok [ p [] [ text "Foo & Bar" ] ])
41 | |> asTest "should handle Gitbook page HTML entities in Markdown string"
42 | ]
43 | ]
44 |
--------------------------------------------------------------------------------
/src/Data/Scoring.elm:
--------------------------------------------------------------------------------
1 | module Data.Scoring exposing
2 | ( Scoring
3 | , compute
4 | , empty
5 | )
6 |
7 | import Data.Impact as Impact exposing (Impacts)
8 | import Data.Impact.Definition as Definition exposing (Definitions)
9 | import Data.Unit as Unit
10 | import Quantity
11 |
12 |
13 | type alias Scoring =
14 | { all : Unit.Impact
15 | , allWithoutComplements : Unit.Impact
16 | , complements : Unit.Impact
17 | , climate : Unit.Impact
18 | , biodiversity : Unit.Impact
19 | , health : Unit.Impact
20 | , resources : Unit.Impact
21 | }
22 |
23 |
24 | compute : Definitions -> Unit.Impact -> Impacts -> Scoring
25 | compute definitions totalComplementsImpactPerKg perKgWithoutComplements =
26 | let
27 | ecsPerKgWithoutComplements =
28 | perKgWithoutComplements
29 | |> Impact.getImpact Definition.Ecs
30 |
31 | subScores =
32 | perKgWithoutComplements
33 | |> Impact.toProtectionAreas definitions
34 | in
35 | { all = Quantity.difference ecsPerKgWithoutComplements totalComplementsImpactPerKg
36 | , allWithoutComplements = ecsPerKgWithoutComplements
37 | , complements = totalComplementsImpactPerKg
38 | , climate = subScores.climate
39 | , biodiversity = subScores.biodiversity
40 | , health = subScores.health
41 | , resources = subScores.resources
42 | }
43 |
44 |
45 | empty : Scoring
46 | empty =
47 | { all = Unit.impact 0
48 | , allWithoutComplements = Unit.impact 0
49 | , complements = Unit.impact 0
50 | , climate = Unit.impact 0
51 | , biodiversity = Unit.impact 0
52 | , health = Unit.impact 0
53 | , resources = Unit.impact 0
54 | }
55 |
--------------------------------------------------------------------------------
/public/pages/accessibilité.md:
--------------------------------------------------------------------------------
1 | # Déclaration d’accessibilité
2 |
3 | Établie le 19 avril 2022.
4 |
5 | La Fabrique Numérique du Ministère de la Transition écologique et solidaire et du Ministère de la Cohésion
6 | des territoires et des Relations avec les collectivités territoriales s’engage à rendre son service
7 | accessible, conformément à l’article 47 de la loi n° 2005-102 du 11 février 2005.
8 |
9 | Cette déclaration d’accessibilité s’applique à **Ecobalyse** (ecobalyse.beta.gouv.fr).
10 |
11 | ## État de conformité
12 |
13 | **Ecobalyse** est **non conforme** avec le Référentiel Général d’Amélioration de l’Accessibilité (RGAA).
14 | Le site n’a encore pas été audité.
15 |
16 | ## Amélioration et contact
17 |
18 | Si vous n’arrivez pas à accéder à un contenu ou à un service, vous pouvez contacter le responsable du service
19 | pour être orienté vers une alternative accessible ou obtenir le contenu sous une autre forme.
20 |
21 | - E-mail : [ecobalyse@beta.gouv.fr](mailto:ecobalyse@beta.gouv.fr?Subject=Accessibilité)
22 |
23 | ## Voie de recours
24 |
25 | Cette procédure est à utiliser dans le cas suivant : vous avez signalé au responsable du site internet
26 | un défaut d’accessibilité qui vous empêche d’accéder à un contenu ou à un des services du portail et vous
27 | n’avez pas obtenu de réponse satisfaisante.
28 |
29 | Vous pouvez :
30 |
31 | - Écrire un message au [Défenseur des droits](https://formulaire.defenseurdesdroits.fr/)
32 | - Contacter [le délégué du Défenseur des droits dans votre région](https://www.defenseurdesdroits.fr/saisir/delegues)
33 | - Envoyer un courrier par la poste (gratuit, ne pas mettre de timbre) :
34 | > Défenseur des droits
35 | > Libre réponse 71120
36 | > 75342 Paris CEDEX 07
37 |
--------------------------------------------------------------------------------
/src/Data/AutocompleteSelector.elm:
--------------------------------------------------------------------------------
1 | module Data.AutocompleteSelector exposing (init)
2 |
3 | import Autocomplete exposing (Autocomplete)
4 | import String.Normalize as Normalize
5 | import Task
6 |
7 |
8 | init : (element -> String) -> List element -> Autocomplete element
9 | init toString availableElements =
10 | Autocomplete.init
11 | { query = ""
12 | , choices = availableElements
13 | , ignoreList = []
14 | }
15 | (\lastChoices ->
16 | Task.succeed
17 | { lastChoices
18 | | choices =
19 | availableElements
20 | |> getChoices toString lastChoices.query
21 | }
22 | )
23 |
24 |
25 | getChoices : (element -> String) -> String -> List element -> List element
26 | getChoices toString query =
27 | let
28 | toWords =
29 | String.toLower
30 | >> Normalize.removeDiacritics
31 | >> String.foldl
32 | (\c acc ->
33 | if not (List.member c [ '(', ')' ]) then
34 | String.cons c acc
35 |
36 | else
37 | acc
38 | )
39 | ""
40 | >> String.split " "
41 |
42 | searchWords =
43 | toWords (String.trim query)
44 | in
45 | List.map (\element -> ( toWords (toString element), element ))
46 | >> List.filter
47 | (\( words, _ ) ->
48 | query == "" || List.all (\w -> List.any (String.contains w) words) searchWords
49 | )
50 | >> List.sortBy (Tuple.second >> toString)
51 | >> List.map Tuple.second
52 |
--------------------------------------------------------------------------------
/src/Data/Matomo.elm:
--------------------------------------------------------------------------------
1 | module Data.Matomo exposing (Stat, decodeStats, encodeStats)
2 |
3 | import Dict
4 | import Iso8601
5 | import Json.Decode as Decode exposing (Decoder)
6 | import Json.Encode as Encode
7 | import Result.Extra as RE
8 | import Time exposing (Posix)
9 |
10 |
11 | type alias Stat =
12 | { label : String
13 | , hits : Int
14 | , time : Posix
15 | }
16 |
17 |
18 | decodeStats : String -> Decoder (List Stat)
19 | decodeStats key =
20 | Decode.dict
21 | (Decode.oneOf
22 | [ Decode.at [ key ] Decode.int
23 |
24 | -- When no data is available at a given date, assume no traffic
25 | , Decode.succeed 0
26 | ]
27 | )
28 | |> Decode.andThen
29 | (Dict.toList
30 | >> List.map
31 | (\( label, hits ) ->
32 | Iso8601.toTime label
33 | |> Result.map (Stat label hits)
34 | |> Result.mapError (always ("Format de date invalide: " ++ label))
35 | )
36 | >> RE.combine
37 | >> (\res ->
38 | case res of
39 | Ok list ->
40 | Decode.succeed list
41 |
42 | Err err ->
43 | Decode.fail err
44 | )
45 | )
46 |
47 |
48 | encodeStats : List Stat -> String
49 | encodeStats stats =
50 | stats
51 | |> Encode.list
52 | (\{ time, hits } ->
53 | -- The format for Highcharts' line chart is [[timestamp, value], …]
54 | Encode.list Encode.int [ Time.posixToMillis time, hits ]
55 | )
56 | |> Encode.encode 0
57 |
--------------------------------------------------------------------------------
/data/import_ecoinvent.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from bw2data.project import projects
4 | from common.import_ import add_missing_substances
5 | from zipfile import ZipFile
6 | import bw2data
7 | import bw2io
8 | import os
9 | import shutil
10 |
11 | PROJECT = "textile"
12 | # Ecoinvent
13 | DATAPATH = "./ECOINVENT3.9.1.zip"
14 | DBNAME = "Ecoinvent 3.9.1"
15 | BIOSPHERE = "biosphere3"
16 |
17 |
18 | def import_ecoinvent(datapath=DATAPATH, project=PROJECT, dbname=DBNAME):
19 | """
20 | Import file at path `datapath` into database named `dbname` in the project
21 | """
22 | projects.set_current(project)
23 | # projects.create_project(project, activate=True, exist_ok=True)
24 |
25 | # unzip
26 | with ZipFile(datapath) as zf:
27 | print("### Extracting the zip file...")
28 | zf.extractall()
29 | unzipped = datapath[0:-4]
30 |
31 | print(f"### Importing {dbname} database from {unzipped}...")
32 | ecoinvent = bw2io.importers.SingleOutputEcospold2Importer(
33 | os.path.join(unzipped, "datasets"), dbname
34 | )
35 | shutil.rmtree(unzipped)
36 | ecoinvent.apply_strategies()
37 | ecoinvent.add_unlinked_flows_to_biosphere_database()
38 | ecoinvent.write_database()
39 | print(f"### Finished importing {dbname}")
40 |
41 |
42 | def main():
43 | projects.set_current(PROJECT)
44 | # projects.create_project(PROJECT, activate=True, exist_ok=True)
45 | bw2data.preferences["biosphere_database"] = BIOSPHERE
46 | bw2io.bw2setup()
47 | add_missing_substances(PROJECT, BIOSPHERE)
48 |
49 | # Import Ecoinvent
50 | if DBNAME not in bw2data.databases:
51 | import_ecoinvent()
52 | else:
53 | print(f"### {DBNAME} is already imported")
54 |
55 |
56 | if __name__ == "__main__":
57 | main()
58 |
--------------------------------------------------------------------------------
/src/Data/Food/Db.elm:
--------------------------------------------------------------------------------
1 | module Data.Food.Db exposing
2 | ( Db
3 | , buildFromJson
4 | )
5 |
6 | import Data.Country exposing (Country)
7 | import Data.Food.Ingredient as Ingredient exposing (Ingredient)
8 | import Data.Food.Process as Process exposing (Process)
9 | import Data.Impact.Definition exposing (Definitions)
10 | import Data.Textile.Db as TextileDb
11 | import Data.Transport as Transport
12 | import Json.Decode as Decode
13 | import Json.Decode.Extra as DE
14 |
15 |
16 | type alias Db =
17 | { -- Common datasources
18 | impactDefinitions : Definitions
19 | , countries : List Country
20 | , transports : Transport.Distances
21 |
22 | -- Food specific datasources
23 | , processes : List Process
24 | , ingredients : List Ingredient
25 | , wellKnown : Process.WellKnown
26 | }
27 |
28 |
29 | buildFromJson : TextileDb.Db -> String -> String -> Result String Db
30 | buildFromJson { impactDefinitions, countries, transports } foodProcessesJson ingredientsJson =
31 | foodProcessesJson
32 | |> Decode.decodeString (Process.decodeList impactDefinitions)
33 | |> Result.andThen
34 | (\processes ->
35 | ingredientsJson
36 | |> Decode.decodeString
37 | (Ingredient.decodeIngredients processes
38 | |> Decode.andThen
39 | (\ingredients ->
40 | Process.loadWellKnown processes
41 | |> Result.map (Db impactDefinitions countries transports processes ingredients)
42 | |> DE.fromResult
43 | )
44 | )
45 | )
46 | |> Result.mapError Decode.errorToString
47 |
--------------------------------------------------------------------------------
/data/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM jupyter/minimal-notebook:notebook-7.0.6
2 |
3 | ENV BRIGHTWAY2_DIR /home/$NB_USER/data
4 | ENV BRIGHTWAY2_DOCKER 1
5 | ENV BRIGHTWAY2_OUTPUT_DIR /home/$NB_USER/output
6 | ENV XDG_CACHE_HOME="/home/${NB_USER}/.cache/"
7 |
8 | USER $NB_USER
9 |
10 | RUN mkdir -p /home/$NB_USER/data \
11 | && mkdir -p /home/$NB_USER/notebooks \
12 | && mkdir -p /home/$NB_USER/output \
13 | && fix-permissions "/home/${NB_USER}/data" \
14 | && fix-permissions "/home/${NB_USER}/notebooks" \
15 | && fix-permissions "/home/${NB_USER}/output"
16 |
17 | # keep in sync with requirements.txt
18 | # allow to update the image if the source repo is updated
19 | ADD https://api.github.com/repos/ccomb/brightway2-io/git/refs/tags/ccomb-2 bw2-io.json
20 | ADD https://api.github.com/repos/brightway-lca/brightway2-parameters/git/refs/tags/1.1.0 bw2-parameters.json
21 | ADD https://api.github.com/repos/brightway-lca/brightway2-data/git/refs/tags/4.0.DEV33 bw2-data.json
22 | ADD https://api.github.com/repos/brightway-lca/brightway2-calc/git/refs/tags/2.0.DEV16 bw2-calc.json
23 | ADD https://api.github.com/repos/brightway-lca/brightway2-analyzer/git/refs/tags/0.11.7 bw2-analyzer.json
24 | ADD https://api.github.com/repos/brightway-lca/bw_projects/git/refs/tags/v2.1.0 bw_projects.json
25 |
26 | COPY requirements.txt .
27 |
28 | USER root
29 | RUN apt update \
30 | && apt install -y gosu vim \
31 | && pip install -r /home/$NB_USER/requirements.txt \
32 | && rm /home/$NB_USER/requirements.txt \
33 | && if [ "$(dpkg --print-architecture)" = "amd64" ]; then pip install pypardiso==0.4; ldconfig; fi;
34 |
35 | COPY simapro-biosphere.json /opt/conda/lib/python3.11/site-packages/bw2io/data/
36 | COPY entrypoint.sh /
37 |
38 | VOLUME /home/$NB_USER
39 | WORKDIR /home/$NB_USER/notebooks
40 | ENTRYPOINT ["/entrypoint.sh"]
41 |
--------------------------------------------------------------------------------
/lib/charts/index.js:
--------------------------------------------------------------------------------
1 | import Highcharts from "highcharts";
2 | import enableA11y from "highcharts/modules/accessibility";
3 | import FoodComparator from "./food-comparator";
4 | import PefPie from "./pefpie";
5 | import Stats from "./stats";
6 |
7 | // Enable a11y https://www.highcharts.com/docs/accessibility/accessibility-module
8 | enableA11y(Highcharts);
9 |
10 | Highcharts.setOptions({
11 | lang: {
12 | loading: "Chargement…",
13 | months: [
14 | "janvier",
15 | "février",
16 | "mars",
17 | "avril",
18 | "mai",
19 | "juin",
20 | "juillet",
21 | "août",
22 | "septembre",
23 | "octobre",
24 | "novembre",
25 | "décembre",
26 | ],
27 | weekdays: ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"],
28 | shortMonths: [
29 | "jan",
30 | "fév",
31 | "mar",
32 | "avr",
33 | "mai",
34 | "juin",
35 | "juil",
36 | "aoû",
37 | "sep",
38 | "oct",
39 | "nov",
40 | "déc",
41 | ],
42 | exportButtonTitle: "Exporter",
43 | printButtonTitle: "Imprimer",
44 | rangeSelectorFrom: "Du",
45 | rangeSelectorTo: "au",
46 | rangeSelectorZoom: "Période",
47 | downloadPNG: "Télécharger en PNG",
48 | downloadJPEG: "Télécharger en JPEG",
49 | downloadPDF: "Télécharger en PDF",
50 | downloadSVG: "Télécharger en SVG",
51 | resetZoom: "Réinitialiser le zoom",
52 | resetZoomTitle: "Réinitialiser le zoom",
53 | thousandsSep: " ",
54 | decimalPoint: ",",
55 | },
56 | });
57 |
58 | export default {
59 | registerElements: function () {
60 | customElements.define("chart-pefpie", PefPie);
61 | customElements.define("chart-stats", Stats);
62 | customElements.define("chart-food-comparator", FoodComparator);
63 | },
64 | };
65 |
--------------------------------------------------------------------------------
/tests/Request/VersionTest.elm:
--------------------------------------------------------------------------------
1 | module Request.VersionTest exposing (..)
2 |
3 | import Expect
4 | import Http exposing (Error(..))
5 | import RemoteData exposing (RemoteData(..))
6 | import Request.Version exposing (Version(..), updateVersion)
7 | import Test exposing (..)
8 | import TestUtils exposing (asTest)
9 |
10 |
11 | suite : Test
12 | suite =
13 | describe "updateVersion"
14 | -- Failed poll
15 | [ updateVersion Unknown (Failure (BadBody "bad body"))
16 | |> Expect.equal Unknown
17 | |> asTest "should leave the current version unchanged (Unknown) if the poll failed"
18 | , updateVersion (Version "hash") (Failure (BadBody "bad body"))
19 | |> Expect.equal (Version "hash")
20 | |> asTest "should leave the current version unchanged (Version ...) if the poll failed"
21 | , updateVersion NewerVersion (Failure (BadBody "bad body"))
22 | |> Expect.equal NewerVersion
23 | |> asTest "should leave the current version unchanged (NewerVersion) if the poll failed"
24 |
25 | -- Successful poll
26 | , updateVersion Unknown (Success "hash")
27 | |> Expect.equal (Version "hash")
28 | |> asTest "should go from Unknown to Version ..."
29 | , updateVersion (Version "hash") (Success "hash")
30 | |> Expect.equal (Version "hash")
31 | |> asTest "should leave the version unchanged if it didn't change"
32 | , updateVersion (Version "hash1") (Success "hash2")
33 | |> Expect.equal NewerVersion
34 | |> asTest "should change to NewerVersion if the version changed"
35 | , updateVersion NewerVersion (Success "hash")
36 | |> Expect.equal NewerVersion
37 | |> asTest "should leave the current version unchanged (NewerVersion)"
38 | ]
39 |
--------------------------------------------------------------------------------
/public/pages/mentions-légales.md:
--------------------------------------------------------------------------------
1 | # Mentions légales
2 |
3 | ## Éditeur de la Plateforme
4 |
5 | Ecobalyse est édité par la Fabrique Numérique du Ministère de la Transition écologique et solidaire et
6 | du Ministère de la Cohésion des territoires et des Relations avec les collectivités territoriales (Grande
7 | Arche de la Défense, 92055 La Défense CEDEX), avec l'appui de
8 | [l’incubateur de services numériques beta.gouv.fr](https://beta.gouv.fr/) de la direction
9 | interministérielle du numérique (DINUM).
10 |
11 | ## Directeur de la publication
12 |
13 | Mr Pascal Dagras.
14 |
15 | ## Hébergement de la Plateforme
16 |
17 | Le site Ecobalyse (ecobalyse.beta.gouv.fr) est hébergé par la société [Scalingo SAS](https://scalingo.com/fr),
18 | inscrite au RCS (Strasbourg B 808 665 483) et dont les serveurs se situent en France.
19 |
20 | - SIREN : 808665483
21 | - Siège social : 15 avenue du Rhin, 67100 Strasbourg, France.
22 |
23 | ## Accessibilité
24 |
25 | Voir la page [Déclaration d’accessibilité](/#/pages/accessibilit%C3%A9).
26 |
27 | ## Signaler un dysfonctionnement
28 |
29 | Si vous rencontrez un défaut d’accessibilité vous empêchant d’accéder à un contenu ou une fonctionnalité
30 | du site, merci de nous en faire part. Si vous n’obtenez pas de réponse rapide de notre part, vous êtes en
31 | droit de faire parvenir vos doléances ou une demande de saisine au Défenseur des droits.
32 |
33 | ## Politique de confidentialité
34 |
35 | Le site Ecobalyse ne collecte ni n'héberge aucune donnée à caractère personnel.
36 |
37 | # Sécurité
38 |
39 | Le site est protégé par un certificat électronique, matérialisé pour la grande majorité des navigateurs par un cadenas. Cette protection participe à la confidentialité des échanges. En aucun cas les services associés à la plateforme ne seront à l’origine d’envoi de courriels pour demander la saisie d’informations personnelles.
40 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 | workflow_dispatch:
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | node-version: [16.x]
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 |
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v2
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: 'npm'
28 |
29 | - name: Cache node_modules
30 | id: cache-node_modules
31 | uses: actions/cache@v2
32 | with:
33 | path: node_modules
34 | key: node_modules-${{ hashFiles('package.json', 'package-lock.json') }}
35 |
36 | - name: Cache ~/.elm
37 | # see https://docs.microsoft.com/en-us/answers/questions/510640/deploy-elm-app-to-azure-static-website-from-github.html
38 | uses: actions/cache@v2
39 | with:
40 | path: ~/.elm
41 | key: elm-cache-${{ hashFiles('elm.json') }}
42 | restore-keys: elm-cache-
43 |
44 | - name: Install dependencies
45 | run: npm ci --prefer-offline --no-audit
46 |
47 | - name: Build app
48 | run: npm run build --if-present
49 |
50 | - name: Build Elm static Db
51 | run: npm run db:build
52 |
53 | - name: Run prettier formatting check
54 | run: npm run format:check
55 |
56 | - name: Run elm-review
57 | run: npm run test:review
58 |
59 | - name: Run client tests
60 | run: npm run test:client
61 |
62 | - name: Run server tests
63 | run: npm run test:server
64 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | const matomo = require("piwik");
2 |
3 | function setupTracker(spec) {
4 | const tracker = matomo.setup(`https://${process.env.MATOMO_HOST}`, process.env.MATOMO_TOKEN);
5 |
6 | function toMatomoCvar(data) {
7 | return data.reduce((acc, entry, index) => {
8 | if (entry.value !== undefined) {
9 | acc[index + 1] = [entry.name, entry.value];
10 | }
11 | return acc;
12 | }, {});
13 | }
14 |
15 | function getQueryParams(query) {
16 | return Object.keys(spec.components.parameters).map((p) => {
17 | const name = spec.components.parameters[p].name.replace("[]", "");
18 | return { name: `query.${name}`, value: query[name] };
19 | });
20 | }
21 |
22 | return {
23 | track(status, req) {
24 | if (process.env.NODE_ENV !== "production") {
25 | return;
26 | }
27 | try {
28 | // https://developer.matomo.org/api-reference/tracking-api
29 | const payload = {
30 | url: "https://ecobalyse.beta.gouv.fr/api" + req.url,
31 | idsite: process.env.MATOMO_SITE_ID,
32 | action_name: "ApiRequest",
33 | e_a: "ApiRequest",
34 | idgoal: 1, // "API" goal on Matomo
35 | _rcn: "API",
36 | _cvar: toMatomoCvar(
37 | [
38 | { name: "http.method", value: req.method },
39 | { name: "http.path", value: req.path },
40 | { name: "http.status", value: status },
41 | ].concat(getQueryParams(req.query)),
42 | ),
43 | ua: req.header("User-Agent"),
44 | lang: req.header("Accept-Language"),
45 | rand: Math.floor(Math.random() * 10000000),
46 | rec: 1,
47 | };
48 | tracker.track(payload, (err) => {
49 | if (err) {
50 | console.error(err);
51 | }
52 | });
53 | } catch (err) {
54 | console.error(err);
55 | }
56 | },
57 | };
58 | }
59 |
60 | module.exports = {
61 | setupTracker,
62 | };
63 |
--------------------------------------------------------------------------------
/src/Views/Icon.elm:
--------------------------------------------------------------------------------
1 | module Views.Icon exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 |
6 |
7 | icon : String -> Html msg
8 | icon name =
9 | i [ attribute "aria-hidden" "true", class ("icon icon-" ++ name) ] []
10 |
11 |
12 | boat : Html msg
13 | boat =
14 | icon "ship"
15 |
16 |
17 | boatCooled : Html msg
18 | boatCooled =
19 | i [ attribute "aria-hidden" "true", class "icon icon-ship" ]
20 | [ snow ]
21 |
22 |
23 | build : Html msg
24 | build =
25 | icon "build"
26 |
27 |
28 | bus : Html msg
29 | bus =
30 | icon "truck"
31 |
32 |
33 | busCooled : Html msg
34 | busCooled =
35 | i [ attribute "aria-hidden" "true", class "icon icon-truck" ]
36 | [ snow ]
37 |
38 |
39 | check : Html msg
40 | check =
41 | icon "check"
42 |
43 |
44 | checkCircle : Html msg
45 | checkCircle =
46 | icon "check-circle"
47 |
48 |
49 | clipboard : Html msg
50 | clipboard =
51 | icon "clipboard"
52 |
53 |
54 | exclamation : Html msg
55 | exclamation =
56 | icon "exclamation"
57 |
58 |
59 | ham : Html msg
60 | ham =
61 | icon "ham"
62 |
63 |
64 | info : Html msg
65 | info =
66 | icon "info"
67 |
68 |
69 | lock : Html msg
70 | lock =
71 | icon "lock"
72 |
73 |
74 | plane : Html msg
75 | plane =
76 | icon "plane"
77 |
78 |
79 | plus : Html msg
80 | plus =
81 | icon "plus"
82 |
83 |
84 | question : Html msg
85 | question =
86 | icon "question"
87 |
88 |
89 | search : Html msg
90 | search =
91 | icon "search"
92 |
93 |
94 | snow : Html msg
95 | snow =
96 | icon "snow"
97 |
98 |
99 | stats : Html msg
100 | stats =
101 | icon "stats"
102 |
103 |
104 | trash : Html msg
105 | trash =
106 | icon "trash"
107 |
108 |
109 | warning : Html msg
110 | warning =
111 | icon "warning"
112 |
113 |
114 | zoomin : Html msg
115 | zoomin =
116 | icon "zoomin"
117 |
118 |
119 | zoomout : Html msg
120 | zoomout =
121 | icon "zoomout"
122 |
--------------------------------------------------------------------------------
/src/Page/Explore/TextileProcesses.elm:
--------------------------------------------------------------------------------
1 | module Page.Explore.TextileProcesses exposing (table)
2 |
3 | import Data.Dataset as Dataset
4 | import Data.Scope exposing (Scope)
5 | import Data.Textile.Process as Process exposing (Process)
6 | import Html exposing (..)
7 | import Html.Attributes exposing (..)
8 | import Page.Explore.Table exposing (Table)
9 | import Route
10 |
11 |
12 | table : { detailed : Bool, scope : Scope } -> Table Process String msg
13 | table { detailed, scope } =
14 | { toId = .uuid >> Process.uuidToString
15 | , toRoute = .uuid >> Just >> Dataset.TextileProcesses >> Route.Explore scope
16 | , rows =
17 | [ { label = "Étape"
18 | , toValue = .stepUsage
19 | , toCell = .stepUsage >> text
20 | }
21 | , { label = "Identifiant"
22 | , toValue = .uuid >> Process.uuidToString
23 | , toCell =
24 | .uuid
25 | >> Process.uuidToString
26 | >> (\uuid ->
27 | if detailed then
28 | code [] [ text uuid ]
29 |
30 | else
31 | a [ Route.href (Route.Explore scope (Dataset.TextileProcesses (Just (Process.Uuid uuid)))) ]
32 | [ code [] [ text uuid ] ]
33 | )
34 | }
35 | , { label = "Nom"
36 | , toValue = .name
37 | , toCell = .name >> text
38 | }
39 | , { label = "Source"
40 | , toValue = .source
41 | , toCell =
42 | \process ->
43 | span [ title process.source ] [ text process.source ]
44 | }
45 | , { label = "Correctif"
46 | , toValue = .correctif
47 | , toCell =
48 | \process ->
49 | span [ title process.correctif ] [ text process.correctif ]
50 | }
51 | , { label = "Unité"
52 | , toValue = .unit
53 | , toCell = .unit >> text
54 | }
55 | ]
56 | }
57 |
--------------------------------------------------------------------------------
/data/bwapi/README.txt:
--------------------------------------------------------------------------------
1 | This a reimplementation of the Brightway explorer through an API, aimed at
2 | being less dependent from Jupyter and Python. Note that this correspond to the
3 | direct export of the Brightway data.
4 |
5 | # Sample list of API endpoints:
6 |
7 |
8 | ## Projects and databases
9 |
10 | * The list of Brightway projects
11 |
12 | https://bwapi.ecobalyse.fr/projects
13 |
14 | * The list of Databases in the project `food`
15 |
16 | https://bwapi.ecobalyse.fr/food/databases
17 |
18 |
19 | ## Search
20 |
21 | * A search of the term `coffee` in the database `Agribalyse 3.1.1`
22 |
23 | https://bwapi.ecobalyse.fr/food/Agribalyse%203.1.1/search/?q=coffee
24 |
25 | ## Activity data and graph
26 |
27 | * The full data of the activity with code `109d03783d26742b87f1a94889d972f3`
28 |
29 | https://bwapi.ecobalyse.fr/food/Agribalyse%203.1.1/109d03783d26742b87f1a94889d972f3/data
30 |
31 | * The first level of the upstream activities of the same activity
32 |
33 | https://bwapi.ecobalyse.fr/food/Agribalyse%203.1.1/109d03783d26742b87f1a94889d972f3/technosphere
34 |
35 | * The elementary flows of the same activity
36 |
37 | https://bwapi.ecobalyse.fr/food/Agribalyse%203.1.1/109d03783d26742b87f1a94889d972f3/biosphere
38 |
39 | * The substitution exchanges of the same activity
40 |
41 | https://bwapi.ecobalyse.fr/food/Agribalyse%203.1.1/109d03783d26742b87f1a94889d972f3/substitution
42 |
43 | * The impacts of the same activity
44 |
45 | https://bwapi.ecobalyse.fr/food/Agribalyse%203.1.1/109d03783d26742b87f1a94889d972f3/impacts/EF%20v3.1
46 |
47 |
48 | ## Methods
49 |
50 | * The list of LCIA methods
51 |
52 | https://bwapi.ecobalyse.fr/food/methods
53 |
54 | * The list of impact categories for a method
55 |
56 | https://bwapi.ecobalyse.fr/food/methods/EF%20v3.1
57 |
58 | * The details of a method
59 |
60 | https://bwapi.ecobalyse.fr/food/methods/EF%20v3.1/acidification/accumulated%20exceedance%20(AE)
61 |
62 | * The characterization factors of a method
63 |
64 | https://bwapi.ecobalyse.fr/food/characterization_factors/EF%20v3.1/acidification/accumulated%20exceedance%20(AE)
65 |
66 |
67 |
--------------------------------------------------------------------------------
/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "application",
3 | "source-directories": ["src"],
4 | "elm-version": "0.19.1",
5 | "dependencies": {
6 | "direct": {
7 | "NoRedInk/elm-json-decode-pipeline": "1.0.1",
8 | "NoRedInk/elm-sortable-table": "1.0.0",
9 | "chain-partners/elm-bignum": "1.0.1",
10 | "cuducos/elm-format-number": "8.1.4",
11 | "dillonkearns/elm-markdown": "7.0.1",
12 | "elm/browser": "1.0.2",
13 | "elm/core": "1.0.5",
14 | "elm/html": "1.0.0",
15 | "elm/http": "2.0.0",
16 | "elm/json": "1.1.3",
17 | "elm/regex": "1.0.0",
18 | "elm/time": "1.0.0",
19 | "elm/url": "1.0.0",
20 | "elm-community/json-extra": "4.3.0",
21 | "elm-community/list-extra": "8.7.0",
22 | "elm-community/result-extra": "2.4.0",
23 | "elm-community/string-extra": "4.0.1",
24 | "f0i/debug-to-json": "1.0.6",
25 | "futureworkz/elm-autocomplete": "1.0.2",
26 | "gingko/time-distance": "2.4.0",
27 | "ianmackenzie/elm-units": "2.9.0",
28 | "krisajenkins/remotedata": "6.0.1",
29 | "kuon/elm-string-normalize": "1.0.5",
30 | "ohanhi/remotedata-http": "4.0.0",
31 | "rtfeldman/elm-iso8601-date-strings": "1.1.4",
32 | "truqu/elm-base64": "2.0.4",
33 | "turboMaCk/any-dict": "2.6.0"
34 | },
35 | "indirect": {
36 | "elm/bytes": "1.0.8",
37 | "elm/file": "1.0.5",
38 | "elm/parser": "1.1.0",
39 | "elm/virtual-dom": "1.0.3",
40 | "jinjor/elm-debounce": "3.0.0",
41 | "myrho/elm-round": "1.0.5",
42 | "robinheghan/murmur3": "1.0.0",
43 | "rtfeldman/elm-css": "17.1.1",
44 | "rtfeldman/elm-hex": "1.0.0"
45 | }
46 | },
47 | "test-dependencies": {
48 | "direct": {
49 | "elm-explorations/test": "2.1.1"
50 | },
51 | "indirect": {
52 | "elm/random": "1.0.0"
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Data/Textile/HeatSource.elm:
--------------------------------------------------------------------------------
1 | module Data.Textile.HeatSource exposing
2 | ( HeatSource(..)
3 | , decode
4 | , encode
5 | , fromString
6 | , toLabelWithZone
7 | , toString
8 | )
9 |
10 | import Data.Zone as Zone exposing (Zone)
11 | import Json.Decode as Decode exposing (Decoder)
12 | import Json.Decode.Extra as DE
13 | import Json.Encode as Encode
14 |
15 |
16 | type HeatSource
17 | = Coal
18 | | HeavyFuel
19 | | LightFuel
20 | | NaturalGas
21 |
22 |
23 | decode : Decoder HeatSource
24 | decode =
25 | Decode.string
26 | |> Decode.andThen (fromString >> DE.fromResult)
27 |
28 |
29 | encode : HeatSource -> Encode.Value
30 | encode =
31 | toString >> Encode.string
32 |
33 |
34 | fromString : String -> Result String HeatSource
35 | fromString string =
36 | case string of
37 | "coal" ->
38 | Ok Coal
39 |
40 | "heavyfuel" ->
41 | Ok HeavyFuel
42 |
43 | "lightfuel" ->
44 | Ok LightFuel
45 |
46 | "naturalgas" ->
47 | Ok NaturalGas
48 |
49 | _ ->
50 | Err <| "Source de production de vapeur inconnue: " ++ string
51 |
52 |
53 | toLabel : HeatSource -> String
54 | toLabel source =
55 | case source of
56 | Coal ->
57 | "Charbon"
58 |
59 | HeavyFuel ->
60 | "Fioul lourd"
61 |
62 | LightFuel ->
63 | "Fioul léger"
64 |
65 | NaturalGas ->
66 | "Gaz naturel"
67 |
68 |
69 | toLabelWithZone : Zone -> HeatSource -> String
70 | toLabelWithZone zone heatSource =
71 | let
72 | zoneLabel =
73 | case zone of
74 | Zone.Europe ->
75 | " (Europe)"
76 |
77 | _ ->
78 | " (hors Europe)"
79 | in
80 | toLabel heatSource ++ zoneLabel
81 |
82 |
83 | toString : HeatSource -> String
84 | toString source =
85 | case source of
86 | Coal ->
87 | "coal"
88 |
89 | HeavyFuel ->
90 | "heavyfuel"
91 |
92 | LightFuel ->
93 | "lightfuel"
94 |
95 | NaturalGas ->
96 | "naturalgas"
97 |
--------------------------------------------------------------------------------
/src/Views/Transport.elm:
--------------------------------------------------------------------------------
1 | module Views.Transport exposing (entry, viewDetails)
2 |
3 | import Data.Transport exposing (Transport)
4 | import Html exposing (..)
5 | import Html.Attributes exposing (..)
6 | import Length exposing (Length)
7 | import Views.Format as Format
8 | import Views.Icon as Icon
9 |
10 |
11 | type alias Config =
12 | { fullWidth : Bool
13 | , onlyIcons : Bool
14 | , hideNoLength : Bool
15 | , airTransportLabel : Maybe String
16 | , seaTransportLabel : Maybe String
17 | , roadTransportLabel : Maybe String
18 | }
19 |
20 |
21 | viewDetails : Config -> Transport -> List (Html msg)
22 | viewDetails { onlyIcons, hideNoLength, airTransportLabel, seaTransportLabel, roadTransportLabel } { air, sea, seaCooled, road, roadCooled } =
23 | [ { distance = air, icon = Icon.plane, label = Maybe.withDefault "Transport aérien" airTransportLabel }
24 | , { distance = sea, icon = Icon.boat, label = Maybe.withDefault "Transport maritime" seaTransportLabel }
25 | , { distance = seaCooled, icon = Icon.boatCooled, label = "Transport maritime réfrigéré" }
26 | , { distance = road, icon = Icon.bus, label = Maybe.withDefault "Transport routier" roadTransportLabel }
27 | , { distance = roadCooled, icon = Icon.busCooled, label = "Transport routier réfrigéré" }
28 | ]
29 | |> List.filterMap
30 | (\{ distance, icon, label } ->
31 | if Length.inKilometers distance == 0 && hideNoLength then
32 | Nothing
33 |
34 | else
35 | Just <| entry { onlyIcons = onlyIcons, distance = distance, icon = icon, label = label }
36 | )
37 |
38 |
39 | type alias EntryConfig msg =
40 | { onlyIcons : Bool
41 | , distance : Length
42 | , icon : Html msg
43 | , label : String
44 | }
45 |
46 |
47 | entry : EntryConfig msg -> Html msg
48 | entry { onlyIcons, distance, icon, label } =
49 | span
50 | [ class "d-flex align-items-center gap-1", title label ]
51 | [ span [ style "cursor" "help" ] [ icon ]
52 | , if onlyIcons then
53 | text ""
54 |
55 | else
56 | Format.km distance
57 | ]
58 |
--------------------------------------------------------------------------------
/tests/Data/Textile/ProductTest.elm:
--------------------------------------------------------------------------------
1 | module Data.Textile.ProductTest exposing (..)
2 |
3 | import Data.Textile.Inputs as Inputs
4 | import Data.Textile.Product as Product
5 | import Data.Unit as Unit
6 | import Duration
7 | import Expect
8 | import Test exposing (..)
9 | import TestUtils exposing (asTest)
10 |
11 |
12 | sampleQuery : Inputs.Query
13 | sampleQuery =
14 | Inputs.tShirtCotonAsie
15 |
16 |
17 | suite : Test
18 | suite =
19 | describe "Data.Product"
20 | [ describe "customDaysOfWear"
21 | [ { daysOfWear = Duration.days 100, wearsPerCycle = 20 }
22 | |> Product.customDaysOfWear (Just (Unit.quality 1)) Nothing
23 | |> Expect.equal
24 | { daysOfWear = Duration.days 100
25 | , useNbCycles = 5
26 | }
27 | |> asTest "should compute custom number of days of wear"
28 | , { daysOfWear = Duration.days 100, wearsPerCycle = 20 }
29 | |> Product.customDaysOfWear (Just (Unit.quality 0.8)) Nothing
30 | |> Expect.equal
31 | { daysOfWear = Duration.days 80
32 | , useNbCycles = 4
33 | }
34 | |> asTest "should compute custom number of days of wear with custom quality"
35 | , { daysOfWear = Duration.days 100, wearsPerCycle = 20 }
36 | |> Product.customDaysOfWear Nothing (Just (Unit.reparability 1.2))
37 | |> Expect.equal
38 | { daysOfWear = Duration.days 120
39 | , useNbCycles = 6
40 | }
41 | |> asTest "should compute custom number of days of wear with custom reparability"
42 | , { daysOfWear = Duration.days 100, wearsPerCycle = 20 }
43 | |> Product.customDaysOfWear (Just (Unit.quality 1.2)) (Just (Unit.reparability 1.2))
44 | |> Expect.equal
45 | { daysOfWear = Duration.days 144
46 | , useNbCycles = 7
47 | }
48 | |> asTest "should compute custom number of days of wear with custom quality & reparability"
49 | ]
50 | ]
51 |
--------------------------------------------------------------------------------
/lib/charts/food-comparator.js:
--------------------------------------------------------------------------------
1 | import BaseChart from "./base";
2 |
3 | export default class extends BaseChart {
4 | constructor() {
5 | super();
6 | }
7 |
8 | static get observedAttributes() {
9 | return ["data"];
10 | }
11 |
12 | get config() {
13 | return {
14 | chart: {
15 | type: "bar",
16 | height: "100%",
17 | animation: false,
18 | },
19 | title: false,
20 | xAxis: {
21 | categories: [],
22 | tickPosition: "inside",
23 | labels: {
24 | allowOverlap: false,
25 | style: { fontSize: "13px", color: "#333" },
26 | },
27 | },
28 | yAxis: {
29 | title: {
30 | text: "µPts d'impact",
31 | },
32 | },
33 | legend: {
34 | reversed: true,
35 | },
36 | plotOptions: {
37 | animation: false,
38 | series: {
39 | animation: false,
40 | stacking: "normal",
41 | },
42 | },
43 | tooltip: {
44 | valueDecimals: 2,
45 | valueSuffix: " µPt d'impact",
46 | },
47 | series: [],
48 | exporting: {
49 | fallbackToExportServer: false,
50 | chartOptions: {
51 | title: false,
52 | },
53 | filename: "ecobalyse",
54 | },
55 | };
56 | }
57 |
58 | attributeChanged(name, oldValue, newValue) {
59 | if (name === "data") {
60 | const rawData = JSON.parse(newValue);
61 | // Code below will map the JSON data received from Elm to data structures
62 | // expected by Highcharts.
63 | const series = rawData[0].data.map(({ name, color }, idx) => {
64 | return { name, color, data: rawData.map(({ data }) => data[idx].y) };
65 | });
66 | this.chart.update({
67 | xAxis: {
68 | categories: rawData.map(({ label }) => label),
69 | },
70 | });
71 | // Remove all existing series...
72 | while (this.chart.series.length) {
73 | this.chart.series[0].remove();
74 | }
75 | // ... and replace them with fresh ones
76 | for (const serie of series) {
77 | this.chart.addSeries(serie);
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Data/Textile/Db.elm:
--------------------------------------------------------------------------------
1 | module Data.Textile.Db exposing
2 | ( Db
3 | , buildFromJson
4 | )
5 |
6 | import Data.Country as Country exposing (Country)
7 | import Data.Impact.Definition as Definition exposing (Definitions)
8 | import Data.Textile.Material as Material exposing (Material)
9 | import Data.Textile.Process as TextileProcess
10 | import Data.Textile.Product as Product exposing (Product)
11 | import Data.Transport as Transport exposing (Distances)
12 | import Json.Decode as Decode exposing (Decoder)
13 | import Json.Decode.Extra as DE
14 |
15 |
16 | type alias Db =
17 | { impactDefinitions : Definitions
18 | , processes : List TextileProcess.Process
19 | , countries : List Country
20 | , materials : List Material
21 | , products : List Product
22 | , transports : Distances
23 | , wellKnown : TextileProcess.WellKnown
24 | }
25 |
26 |
27 | buildFromJson : String -> Result String Db
28 | buildFromJson json =
29 | Decode.decodeString decode json
30 | |> Result.mapError Decode.errorToString
31 |
32 |
33 | decode : Decoder Db
34 | decode =
35 | Decode.field "impacts" Definition.decode
36 | |> Decode.andThen
37 | (\definitions ->
38 | Decode.field "processes" (TextileProcess.decodeList definitions)
39 | |> Decode.andThen
40 | (\processes ->
41 | Decode.map4 (Db definitions processes)
42 | (Decode.field "countries" (Country.decodeList processes))
43 | (Decode.field "materials" (Material.decodeList processes))
44 | (Decode.field "products" (Product.decodeList processes))
45 | (Decode.field "transports" Transport.decodeDistances)
46 | |> Decode.andThen
47 | (\partiallyLoaded ->
48 | TextileProcess.loadWellKnown processes
49 | |> Result.map partiallyLoaded
50 | |> DE.fromResult
51 | )
52 | )
53 | )
54 |
--------------------------------------------------------------------------------
/src/Page/Editorial.elm:
--------------------------------------------------------------------------------
1 | module Page.Editorial exposing
2 | ( Model
3 | , Msg(..)
4 | , init
5 | , update
6 | , view
7 | )
8 |
9 | import Data.Session exposing (Session)
10 | import Html exposing (..)
11 | import Html.Attributes exposing (..)
12 | import Http
13 | import List.Extra as LE
14 | import Ports
15 | import RemoteData exposing (WebData)
16 | import Views.Alert as Alert
17 | import Views.Container as Container
18 | import Views.Markdown as Markdown
19 | import Views.Spinner as Spinner
20 |
21 |
22 | type alias Model =
23 | { slug : String
24 | , content : WebData String
25 | }
26 |
27 |
28 | type Msg
29 | = ContentReceived (WebData String)
30 |
31 |
32 | init : String -> Session -> ( Model, Session, Cmd Msg )
33 | init slug session =
34 | ( { slug = slug, content = RemoteData.Loading }
35 | , session
36 | , Cmd.batch
37 | [ Ports.scrollTo { x = 0, y = 0 }
38 | , Http.get
39 | { url = "pages/" ++ slug ++ ".md"
40 | , expect =
41 | Http.expectString
42 | (RemoteData.fromResult >> ContentReceived)
43 | }
44 | ]
45 | )
46 |
47 |
48 | update : Session -> Msg -> Model -> ( Model, Session, Cmd Msg )
49 | update session msg model =
50 | case msg of
51 | ContentReceived content ->
52 | ( { model | content = content }, session, Cmd.none )
53 |
54 |
55 | view : Session -> Model -> ( String, List (Html Msg) )
56 | view _ model =
57 | case model.content of
58 | RemoteData.Success content ->
59 | ( content
60 | |> String.split "\n"
61 | |> List.head
62 | |> Maybe.andThen (String.split "# " >> LE.last)
63 | |> Maybe.withDefault "Sans titre"
64 | , [ Container.centered []
65 | [ Markdown.simple [ class "pb-5" ] content
66 | ]
67 | ]
68 | )
69 |
70 | RemoteData.Loading ->
71 | ( "Chargement…", [ Spinner.view ] )
72 |
73 | RemoteData.Failure httpError ->
74 | ( "Erreur de chargement", [ Alert.httpError httpError ] )
75 |
76 | RemoteData.NotAsked ->
77 | ( "", [] )
78 |
--------------------------------------------------------------------------------
/review/src/ReviewConfig.elm:
--------------------------------------------------------------------------------
1 | module ReviewConfig exposing (config)
2 |
3 |
4 | import NoDebug.TodoOrToString
5 | import NoExposingEverything
6 | import NoImportingEverything
7 | import NoMissingTypeAnnotation
8 | import NoRedundantConcat
9 | import NoRedundantCons
10 | import NoUnused.CustomTypeConstructorArgs
11 | import NoUnused.CustomTypeConstructors
12 | import NoUnused.Dependencies
13 | import NoUnused.Exports
14 | import NoUnused.Modules
15 | import NoUnused.Parameters
16 | import NoUnused.Patterns
17 | import NoUnused.Variables
18 | import Review.Rule as Rule exposing (Rule)
19 | import Simplify
20 | import CognitiveComplexity
21 |
22 |
23 | config : List Rule
24 | config =
25 | [ -- CognitiveComplexity
26 | CognitiveComplexity.rule 15
27 | -- NoDebug
28 | , NoDebug.TodoOrToString.rule
29 | |> Rule.ignoreErrorsForFiles [ "src/Views/Debug.elm" ]
30 | -- Common
31 | , NoExposingEverything.rule
32 | |> Rule.ignoreErrorsForFiles [ "src/Data/Color.elm" ]
33 | |> Rule.ignoreErrorsForFiles [ "src/Views/Icon.elm" ]
34 | |> Rule.ignoreErrorsForDirectories [ "tests/" ]
35 | , NoImportingEverything.rule
36 | [ "Html"
37 | , "Html.Attributes"
38 | , "Html.Events"
39 | , "Svg"
40 | , "Svg.Attributes"
41 | ]
42 | |> Rule.ignoreErrorsForDirectories [ "tests/" ]
43 | , NoMissingTypeAnnotation.rule
44 | |> Rule.ignoreErrorsForDirectories [ "tests/" ]
45 | , NoRedundantConcat.rule
46 | , NoRedundantCons.rule
47 | -- NoUnused
48 | , NoUnused.CustomTypeConstructors.rule []
49 | |> Rule.ignoreErrorsForFiles [ "src/Views/Modal.elm" ]
50 | , NoUnused.CustomTypeConstructorArgs.rule
51 | |> Rule.ignoreErrorsForFiles [ "src/Server/Route.elm" ]
52 | |> Rule.ignoreErrorsForFiles [ "src/Views/Page.elm" ]
53 | , NoUnused.Dependencies.rule
54 | , NoUnused.Exports.rule
55 | |> Rule.ignoreErrorsForFiles [ "src/Views/Button.elm" ]
56 | |> Rule.ignoreErrorsForFiles [ "src/Views/Debug.elm" ]
57 | , NoUnused.Modules.rule
58 | |> Rule.ignoreErrorsForFiles [ "src/Views/Debug.elm" ]
59 | , NoUnused.Parameters.rule
60 | , NoUnused.Patterns.rule
61 | , NoUnused.Variables.rule
62 | -- Simlify
63 | , Simplify.rule Simplify.defaults
64 | ]
65 |
--------------------------------------------------------------------------------
/bin/build-db:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * This script generates JSON Db fixtures so tests can perform assertions
5 | * against actual JSON data without having to rely on Http requests, which
6 | * is impossible in an Elm test environment.
7 | */
8 | const fs = require("fs");
9 |
10 | function getJson(path) {
11 | return JSON.parse(fs.readFileSync(path).toString());
12 | }
13 |
14 | /**
15 | * Adapts a standard JSON string to what is expected to be the format
16 | * used in Elm's template strings (`"""{}"""`).
17 | */
18 | function serializeForElmTemplateString(toStringify) {
19 | return JSON.stringify(toStringify).replaceAll("\\", "\\\\");
20 | }
21 |
22 | function buildTextileJsonDb(basePath = "public/data") {
23 | return serializeForElmTemplateString({
24 | // common data
25 | countries: getJson(`${basePath}/countries.json`),
26 | impacts: getJson(`${basePath}/impacts.json`),
27 | transports: getJson(`${basePath}/transports.json`),
28 | // textile data
29 | materials: getJson(`${basePath}/textile/materials.json`),
30 | processes: getJson(`${basePath}/textile/processes.json`),
31 | products: getJson(`${basePath}/textile/products.json`),
32 | });
33 | }
34 |
35 | function buildFoodProcessesJsonDb(basePath = "public/data/food") {
36 | return serializeForElmTemplateString(getJson(`${basePath}/processes.json`));
37 | }
38 |
39 | function buildFoodIngredientsJsonDb(basePath = "public/data/food") {
40 | return serializeForElmTemplateString(getJson(`${basePath}/ingredients.json`));
41 | }
42 |
43 | const targetDbFile = "src/Static/Db.elm";
44 | const elmTemplate = fs.readFileSync(`${targetDbFile}-template`).toString();
45 | const elmWithFixtures = elmTemplate
46 | .replace("%textileJson%", buildTextileJsonDb())
47 | .replace("%foodProcessesJson%", buildFoodProcessesJsonDb())
48 | .replace("%foodIngredientsJson%", buildFoodIngredientsJsonDb());
49 |
50 | const header =
51 | "---- THIS FILE WAS GENERATED FROM THE FILE `Db.elm-template` BY THE `/bin/build-db` SCRIPT";
52 |
53 | try {
54 | fs.writeFileSync(targetDbFile, `${header}\n\n${elmWithFixtures}`);
55 | const fileSizeInKB = Math.ceil(fs.statSync(targetDbFile).size / 1024);
56 | console.log(`Successfully generated Elm static database at ${targetDbFile} (${fileSizeInKB} KB)`);
57 | } catch (err) {
58 | throw new Error(`Unable to generate Elm static database: ${err}`);
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Data/Textile/LifeCycleTest.elm:
--------------------------------------------------------------------------------
1 | module Data.Textile.LifeCycleTest exposing (..)
2 |
3 | import Data.Country as Country
4 | import Data.Textile.Db as TextileDb
5 | import Data.Textile.Inputs as Inputs exposing (tShirtCotonFrance)
6 | import Data.Textile.LifeCycle as LifeCycle exposing (LifeCycle)
7 | import Expect
8 | import Length
9 | import Test exposing (..)
10 | import TestUtils exposing (asTest, suiteWithDb)
11 |
12 |
13 | km : Float -> Length.Length
14 | km =
15 | Length.kilometers
16 |
17 |
18 | lifeCycleToTransports : TextileDb.Db -> Inputs.Query -> LifeCycle -> Result String LifeCycle
19 | lifeCycleToTransports textileDb query lifeCycle =
20 | query
21 | |> Inputs.fromQuery textileDb
22 | |> Result.map
23 | (\materials ->
24 | LifeCycle.computeStepsTransport textileDb materials lifeCycle
25 | )
26 |
27 |
28 | suite : Test
29 | suite =
30 | suiteWithDb "Data.LifeCycle"
31 | (\{ textileDb } ->
32 | [ describe "computeTransportSummary"
33 | [ tShirtCotonFrance
34 | |> LifeCycle.fromQuery textileDb
35 | |> Result.andThen (lifeCycleToTransports textileDb tShirtCotonFrance)
36 | |> Result.map LifeCycle.computeTotalTransportImpacts
37 | |> Result.map (\{ road, sea } -> ( Length.inKilometers road, Length.inKilometers sea ))
38 | |> Expect.equal (Ok ( 3000, 21549 ))
39 | |> asTest "should compute default distances"
40 | , let
41 | tShirtCotonEnnoblementIndia =
42 | { tShirtCotonFrance
43 | | countryFabric = Country.Code "FR"
44 | , countryDyeing = Country.Code "IN" -- Ennoblement in India
45 | , countryMaking = Country.Code "FR"
46 | }
47 | in
48 | tShirtCotonEnnoblementIndia
49 | |> LifeCycle.fromQuery textileDb
50 | |> Result.andThen (lifeCycleToTransports textileDb tShirtCotonEnnoblementIndia)
51 | |> Result.map LifeCycle.computeTotalTransportImpacts
52 | |> Result.map (\{ road, sea } -> ( Length.inKilometers road, Length.inKilometers sea ))
53 | |> Expect.equal (Ok ( 2000, 45471 ))
54 | |> asTest "should compute custom distances"
55 | ]
56 | ]
57 | )
58 |
--------------------------------------------------------------------------------
/tests/Data/TransportTest.elm:
--------------------------------------------------------------------------------
1 | module Data.TransportTest exposing (..)
2 |
3 | import Data.Country as Country
4 | import Data.Impact as Impact exposing (Impacts)
5 | import Data.Scope as Scope
6 | import Data.Transport as Transport exposing (Transport)
7 | import Dict.Any as AnyDict
8 | import Expect
9 | import Length
10 | import Test exposing (..)
11 | import TestUtils exposing (asTest, suiteWithDb)
12 |
13 |
14 | km : Float -> Length.Length
15 | km =
16 | Length.kilometers
17 |
18 |
19 | franceChina : Impacts -> Transport
20 | franceChina impacts =
21 | { road = km 8169
22 | , roadCooled = km 0
23 | , sea = km 21549
24 | , seaCooled = km 0
25 | , air = km 8189
26 | , impacts = impacts
27 | }
28 |
29 |
30 | suite : Test
31 | suite =
32 | suiteWithDb "Data.Transport"
33 | (\{ textileDb } ->
34 | let
35 | defaultImpacts =
36 | Impact.empty
37 | in
38 | [ textileDb.countries
39 | |> List.map
40 | (\{ code } ->
41 | AnyDict.keys textileDb.transports
42 | |> List.member code
43 | |> Expect.equal True
44 | |> asTest (Country.codeToString code ++ "should have transports data available")
45 | )
46 | |> describe "transports data availability checks"
47 | , describe "getTransportBetween"
48 | [ textileDb.transports
49 | |> Transport.getTransportBetween Scope.Textile defaultImpacts (Country.Code "FR") (Country.Code "CN")
50 | |> Expect.equal (franceChina defaultImpacts)
51 | |> asTest "should retrieve distance between two countries"
52 | , textileDb.transports
53 | |> Transport.getTransportBetween Scope.Textile defaultImpacts (Country.Code "CN") (Country.Code "FR")
54 | |> Expect.equal (franceChina defaultImpacts)
55 | |> asTest "should retrieve distance between two swapped countries"
56 | , textileDb.transports
57 | |> Transport.getTransportBetween Scope.Textile defaultImpacts (Country.Code "FR") (Country.Code "FR")
58 | |> Expect.equal (Transport.defaultInland Scope.Textile defaultImpacts)
59 | |> asTest "should apply default inland transport when country is the same"
60 | ]
61 | ]
62 | )
63 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '22 6 * * 4'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v2
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v1
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v1
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v1
71 |
--------------------------------------------------------------------------------
/src/Page/Explore/FoodProcesses.elm:
--------------------------------------------------------------------------------
1 | module Page.Explore.FoodProcesses exposing (table)
2 |
3 | import Data.Dataset as Dataset
4 | import Data.Food.Db as FoodDb
5 | import Data.Food.Process as FoodProcess
6 | import Data.Scope exposing (Scope)
7 | import Html exposing (..)
8 | import Page.Explore.Table exposing (Table)
9 | import Route
10 |
11 |
12 | table : FoodDb.Db -> { detailed : Bool, scope : Scope } -> Table FoodProcess.Process String msg
13 | table _ { detailed, scope } =
14 | { toId = .code >> FoodProcess.codeToString
15 | , toRoute = .code >> Just >> Dataset.FoodProcesses >> Route.Explore scope
16 | , rows =
17 | [ { label = "Identifiant"
18 | , toValue = .code >> FoodProcess.codeToString
19 | , toCell =
20 | \process ->
21 | if detailed then
22 | code [] [ text (FoodProcess.codeToString process.code) ]
23 |
24 | else
25 | a [ Route.href (Route.Explore scope (Dataset.FoodProcesses (Just process.code))) ]
26 | [ code [] [ text (FoodProcess.codeToString process.code) ] ]
27 | }
28 | , { label = "Nom"
29 | , toValue = getDisplayName
30 | , toCell = getDisplayName >> text
31 | }
32 | , { label = "Catégorie"
33 | , toValue = .category >> FoodProcess.categoryToString
34 | , toCell = .category >> FoodProcess.categoryToString >> text
35 | }
36 | , { label = "Nom technique"
37 | , toValue = .name >> FoodProcess.nameToString
38 | , toCell = .name >> FoodProcess.nameToString >> text
39 | }
40 | , { label = "Identifiant source"
41 | , toValue = .code >> FoodProcess.codeToString
42 | , toCell = \process -> code [] [ text (FoodProcess.codeToString process.code) ]
43 | }
44 | , { label = "Unité"
45 | , toValue = .unit
46 | , toCell = .unit >> text
47 | }
48 | , { label = "Description du système"
49 | , toValue = .systemDescription
50 | , toCell = .systemDescription >> text
51 | }
52 | , { label = "Commentaire"
53 | , toValue = .comment >> Maybe.withDefault "N/A"
54 | , toCell = .comment >> Maybe.withDefault "N/A" >> text
55 | }
56 | ]
57 | }
58 |
59 |
60 | getDisplayName : FoodProcess.Process -> String
61 | getDisplayName process =
62 | case process.displayName of
63 | Just displayName ->
64 | displayName
65 |
66 | Nothing ->
67 | FoodProcess.nameToString process.name
68 |
--------------------------------------------------------------------------------
/src/Views/Sidebar.elm:
--------------------------------------------------------------------------------
1 | module Views.Sidebar exposing (Config, view)
2 |
3 | import Data.Bookmark exposing (Bookmark)
4 | import Data.Impact exposing (Impacts)
5 | import Data.Impact.Definition exposing (Definition, Trigram)
6 | import Data.Scope exposing (Scope)
7 | import Data.Session exposing (Session)
8 | import Html exposing (..)
9 | import Html.Attributes exposing (..)
10 | import Mass exposing (Mass)
11 | import Views.Bookmark as BookmarkView
12 | import Views.Impact as ImpactView
13 | import Views.ImpactTabs as ImpactTabs
14 | import Views.Score as ScoreView
15 |
16 |
17 | type alias Config msg =
18 | { session : Session
19 | , scope : Scope
20 |
21 | -- Impact selector
22 | , selectedImpact : Definition
23 | , switchImpact : Result String Trigram -> msg
24 |
25 | -- Score
26 | , productMass : Mass
27 | , totalImpacts : Impacts
28 |
29 | -- Impacts tabs
30 | , impactTabsConfig : ImpactTabs.Config msg
31 |
32 | -- Bookmarks
33 | , activeBookmarkTab : BookmarkView.ActiveTab
34 | , bookmarkName : String
35 | , copyToClipBoard : String -> msg
36 | , compareBookmarks : msg
37 | , deleteBookmark : Bookmark -> msg
38 | , saveBookmark : msg
39 | , updateBookmarkName : String -> msg
40 | , switchBookmarkTab : BookmarkView.ActiveTab -> msg
41 | }
42 |
43 |
44 | view : Config msg -> Html msg
45 | view config =
46 | div
47 | [ class "d-flex flex-column gap-3 mb-3 sticky-md-top"
48 | , style "top" "7px"
49 | ]
50 | [ ImpactView.selector
51 | config.session.textileDb.impactDefinitions
52 | { selectedImpact = config.selectedImpact.trigram
53 | , switchImpact = config.switchImpact
54 | }
55 | , ScoreView.view
56 | { impactDefinition = config.selectedImpact
57 | , score = config.totalImpacts
58 | , mass = config.productMass
59 | }
60 | , config.impactTabsConfig
61 | |> ImpactTabs.view config.session.textileDb.impactDefinitions
62 | , BookmarkView.view
63 | { session = config.session
64 | , activeTab = config.activeBookmarkTab
65 | , bookmarkName = config.bookmarkName
66 | , impact = config.selectedImpact
67 | , scope = config.scope
68 | , copyToClipBoard = config.copyToClipBoard
69 | , compare = config.compareBookmarks
70 | , delete = config.deleteBookmark
71 | , save = config.saveBookmark
72 | , update = config.updateBookmarkName
73 | , switchTab = config.switchBookmarkTab
74 | }
75 | ]
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ecobalyse",
3 | "version": "0.0.1",
4 | "description": "Accélérer l'affichage environnemental de la filière textile française",
5 | "author": "Ecobalyse ",
6 | "license": "MIT",
7 | "private": true,
8 | "scripts": {
9 | "build": "npm run server:build && rimraf dist && npm run build:init && parcel build index.html --public-url ./",
10 | "build:init": "./bin/update-version.sh && mkdir -p dist && cp -r public/* dist/ && npm run db:build",
11 | "db:build": "./bin/build-db",
12 | "format:json": "npx prettier@3.0.3 --write .",
13 | "format:check": "npx prettier@3.0.3 --check .",
14 | "postinstall": "npx husky install",
15 | "server:build": "npm run db:build && elm make src/Server.elm --optimize --output=server-app.js",
16 | "server:dev": "npm run server:build && nodemon server.js --config nodemon.json",
17 | "server:debug": "elm make src/Server.elm --output=server-app.js && nodemon server.js --config nodemon.json",
18 | "server:start": "node server.js",
19 | "start:parcel": "parcel serve index.html --no-cache",
20 | "start:dev": "npm run build:init && concurrently -n \"watch,parcel,server\" -c \"green,cyan\" \"npm run start:parcel\" \"npm run server:dev\"",
21 | "start": "PARCEL_ELM_NO_DEBUG=1 npm run start:dev",
22 | "test:client": "npx elm-test",
23 | "test:review": "npx elm-review",
24 | "test:server": "npm run server:build && jest",
25 | "test": "npm run db:build && npm run test:review && npm run test:client && npm run test:server"
26 | },
27 | "dependencies": {
28 | "@parcel/transformer-elm": "^2.10.3",
29 | "@parcel/transformer-image": "^2.10.3",
30 | "@parcel/transformer-sass": "^2.10.3",
31 | "@sentry/browser": "^7.87.0",
32 | "@sentry/node": "^7.87.0",
33 | "@sentry/tracing": "^7.87.0",
34 | "bootstrap": "^5.3.2",
35 | "cors": "^2.8.5",
36 | "dotenv": "^16.3.1",
37 | "elm": "^0.19.1-6",
38 | "express": "^4.18.2",
39 | "helmet": "^7.1.0",
40 | "highcharts": "^11.2.0",
41 | "js-yaml": "^4.1.0",
42 | "parcel": "^2.10.3",
43 | "piwik": "^1.0.9"
44 | },
45 | "devDependencies": {
46 | "concurrently": "^8.2.2",
47 | "elm-format": "^0.8.7",
48 | "elm-json": "^0.2.13",
49 | "elm-review": "^2.10.3",
50 | "elm-test": "0.19.1-revision12",
51 | "husky": "^8.0.3",
52 | "jest": "^29.7.0",
53 | "nodemon": "^3.0.2",
54 | "npm-check-updates": "^16.14.12",
55 | "prettier": "^3.1.1",
56 | "process": "^0.11.10",
57 | "rimraf": "^5.0.5",
58 | "superagent": "^8.1.2",
59 | "supertest": "^6.3.3"
60 | },
61 | "cacheDirectories": [
62 | "node_modules",
63 | "~/.elm"
64 | ]
65 | }
66 |
--------------------------------------------------------------------------------
/public/icomoon/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'icomoon';
3 | src: url('fonts/icomoon.eot?nj6lan');
4 | src: url('fonts/icomoon.eot?nj6lan#iefix') format('embedded-opentype'),
5 | url('fonts/icomoon.ttf?nj6lan') format('truetype'),
6 | url('fonts/icomoon.woff?nj6lan') format('woff'),
7 | url('fonts/icomoon.svg?nj6lan#icomoon') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | font-display: block;
11 | }
12 |
13 | [class^="icon-"], [class*=" icon-"] {
14 | /* use !important to prevent issues with browser extensions that change fonts */
15 | font-family: 'icomoon' !important;
16 | speak: never;
17 | font-style: normal;
18 | font-weight: normal;
19 | font-variant: normal;
20 | text-transform: none;
21 | line-height: 1;
22 |
23 | /* Better Font Rendering =========== */
24 | -webkit-font-smoothing: antialiased;
25 | -moz-osx-font-smoothing: grayscale;
26 | }
27 |
28 | .icon-leaf:before {
29 | content: "\e901";
30 | }
31 | .icon-search:before {
32 | content: "\e902";
33 | }
34 | .icon-ham:before {
35 | content: "\e918";
36 | }
37 | .icon-external:before {
38 | content: "\e908";
39 | }
40 | .icon-zoomin:before {
41 | content: "\e906";
42 | }
43 | .icon-zoomout:before {
44 | content: "\e907";
45 | }
46 | .icon-atom:before {
47 | content: "\e903";
48 | }
49 | .icon-rail:before {
50 | content: "\e916";
51 | }
52 | .icon-build:before {
53 | content: "\e909";
54 | }
55 | .icon-home:before {
56 | content: "\e90b";
57 | }
58 | .icon-snow:before {
59 | content: "\e917";
60 | }
61 | .icon-plus:before {
62 | content: "\f067";
63 | }
64 | .icon-check:before {
65 | content: "\f00c";
66 | }
67 | .icon-lock:before {
68 | content: "\f023";
69 | }
70 | .icon-check-circle:before {
71 | content: "\f058";
72 | }
73 | .icon-question:before {
74 | content: "\f059";
75 | }
76 | .icon-info:before {
77 | content: "\f05a";
78 | }
79 | .icon-exclamation:before {
80 | content: "\f06a";
81 | }
82 | .icon-warning:before {
83 | content: "\f071";
84 | }
85 | .icon-plane:before {
86 | content: "\f072";
87 | }
88 | .icon-stats:before {
89 | content: "\f080";
90 | }
91 | .icon-truck:before {
92 | content: "\f0d1";
93 | }
94 | .icon-clipboard:before {
95 | content: "\f0ea";
96 | }
97 | .icon-paste:before {
98 | content: "\f0ea";
99 | }
100 | .icon-document:before {
101 | content: "\f15c";
102 | }
103 | .icon-help:before {
104 | content: "\f1cd";
105 | }
106 | .icon-calculator:before {
107 | content: "\f1ec";
108 | }
109 | .icon-trash:before {
110 | content: "\f1f8";
111 | }
112 | .icon-ship:before {
113 | content: "\f21a";
114 | }
115 | .icon-examples:before {
116 | content: "\f277";
117 | }
--------------------------------------------------------------------------------
/tests/Server/ServerTest.elm:
--------------------------------------------------------------------------------
1 | module Server.ServerTest exposing (..)
2 |
3 | import Data.Food.Query as FoodQuery
4 | import Expect
5 | import Json.Encode as Encode
6 | import Server
7 | import Server.Request exposing (Request)
8 | import Test exposing (..)
9 | import TestUtils exposing (asTest, suiteWithDb)
10 |
11 |
12 | suite : Test
13 | suite =
14 | suiteWithDb "Server"
15 | (\dbs ->
16 | [ describe "ports"
17 | -- Note: these prevent false elm-review reports
18 | [ Server.input (always Sub.none)
19 | |> Expect.notEqual Sub.none
20 | |> asTest "should apply input subscription"
21 | , Server.output Encode.null
22 | |> Expect.notEqual Cmd.none
23 | |> asTest "should apply output command"
24 | ]
25 | , describe "handleRequest"
26 | [ "/invalid"
27 | |> request "GET" Encode.null
28 | |> Server.handleRequest dbs
29 | |> Tuple.first
30 | |> Expect.equal 404
31 | |> asTest "should catch invalid endpoints"
32 |
33 | -- GET queries
34 | , "/food/recipe?ingredients[]=invalid"
35 | |> request "GET" Encode.null
36 | |> Server.handleRequest dbs
37 | |> Tuple.first
38 | |> Expect.equal 400
39 | |> asTest "should reject an invalid GET query"
40 | , "/food/recipe?ingredients[]=egg-indoor-code3;120"
41 | |> request "GET" Encode.null
42 | |> Server.handleRequest dbs
43 | |> Tuple.first
44 | |> Expect.equal 200
45 | |> asTest "should accept a valid GET query"
46 |
47 | -- POST queries
48 | , "/food/recipe"
49 | |> request "POST" Encode.null
50 | |> Server.handleRequest dbs
51 | |> Tuple.first
52 | |> Expect.equal 400
53 | |> asTest "should reject an invalid POST query"
54 | , "/food/recipe"
55 | |> request "POST" (FoodQuery.encode FoodQuery.carrotCake)
56 | |> Server.handleRequest dbs
57 | |> Tuple.first
58 | |> Expect.equal 200
59 | |> asTest "should accept a valid POST query"
60 | ]
61 | ]
62 | )
63 |
64 |
65 | request : String -> Encode.Value -> String -> Request
66 | request method body url =
67 | { method = method
68 | , url = url
69 | , body = body
70 | , jsResponseHandler = Encode.null
71 | }
72 |
--------------------------------------------------------------------------------
/data/common/distances/json_to_csv.py:
--------------------------------------------------------------------------------
1 | import json
2 | import pandas as pd
3 | import gettext
4 |
5 | """Convert distances.json to distances.csv, countries.csv to use it in excel
6 | """
7 |
8 | with open("distances.json", "r") as f:
9 | distances = json.load(f)
10 |
11 | data = []
12 |
13 | countries_set = set()
14 | # iterate on countries_from
15 | for country_from, country_from_dict in distances.items():
16 | countries_set.add(country_from)
17 | # iterate on countries_to
18 | for country_to, dist in country_from_dict.items():
19 | countries_set.add(country_to)
20 | route = country_from + "-" + country_to
21 | reverse_route = country_to + "-" + country_from
22 | # check alphabetical order
23 | if country_from < country_to:
24 | country_0 = country_from
25 | country_1 = country_to
26 | else:
27 | country_0 = country_to
28 | country_1 = country_from
29 | route_ordered = country_0 + "-" + country_1
30 | # add row
31 | row = [route, route_ordered, dist["road"], dist["sea"], dist["air"]]
32 | data.append(row)
33 | # add reverse route row
34 | row_reverse = [
35 | reverse_route,
36 | route_ordered,
37 | dist["road"],
38 | dist["sea"],
39 | dist["air"],
40 | ]
41 | data.append(row_reverse)
42 |
43 |
44 | # add 500 km of road distance for all intra country distances
45 | countries_list = list(countries_set)
46 | countries_list = sorted(countries_list)
47 |
48 | for country in countries_list:
49 | route = country + "-" + country
50 | row = [route, route, 500, 0, 0]
51 | data.append(row)
52 |
53 | # csv export
54 | distances_df = pd.DataFrame(
55 | data, columns=["route", "ordered_route", "road", "sea", "air"]
56 | )
57 | distances_df.to_csv("distances.csv", index=False)
58 | countries_importance = pd.read_csv("countries_importance.csv")
59 |
60 | ## build list of countries with alpha 2 correspondance
61 |
62 | # build dic of alpha2 -> country
63 | alpha2_to_country = {}
64 |
65 | for i, x in countries_importance.iterrows():
66 | alpha2_to_country[x["Alpha-2 code"]] = x["Country"]
67 |
68 |
69 | country_alpha2 = []
70 |
71 | french = gettext.translation("iso3166", pycountry.LOCALES_DIR, languages=["fr"])
72 | french.install()
73 | # print list of countries
74 | for alpha2 in countries_list:
75 | py_country = pycountry.countries.get(alpha_2=alpha2)
76 | country_french = _(py_country.name)
77 | row = [alpha2, country_french, py_country.name]
78 | country_alpha2.append(row)
79 |
80 | # csv export
81 | countries_df = pd.DataFrame(country_alpha2, columns=["alpha-2", "name_fr", "name_en"])
82 | countries_df.to_csv("countries_output.csv", index=False, encoding="utf-8")
83 |
--------------------------------------------------------------------------------
/src/Data/Textile/MakingComplexity.elm:
--------------------------------------------------------------------------------
1 | module Data.Textile.MakingComplexity exposing
2 | ( MakingComplexity(..)
3 | , decode
4 | , fromString
5 | , toDuration
6 | , toLabel
7 | , toString
8 | )
9 |
10 | import Duration exposing (Duration)
11 | import Json.Decode as Decode exposing (Decoder)
12 | import Json.Decode.Extra as DE
13 |
14 |
15 | type MakingComplexity
16 | = VeryHigh
17 | | High
18 | | Medium
19 | | Low
20 | | VeryLow
21 | | NotApplicable
22 |
23 |
24 | toDuration : MakingComplexity -> Duration
25 | toDuration makingComplexity =
26 | case makingComplexity of
27 | VeryHigh ->
28 | Duration.minutes 120
29 |
30 | High ->
31 | Duration.minutes 60
32 |
33 | Medium ->
34 | Duration.minutes 30
35 |
36 | Low ->
37 | Duration.minutes 15
38 |
39 | VeryLow ->
40 | Duration.minutes 5
41 |
42 | NotApplicable ->
43 | Duration.minutes 0
44 |
45 |
46 | toLabel : MakingComplexity -> String
47 | toLabel makingComplexity =
48 | case makingComplexity of
49 | VeryHigh ->
50 | "Très élevée"
51 |
52 | High ->
53 | "Elevée"
54 |
55 | Medium ->
56 | "Moyenne"
57 |
58 | Low ->
59 | "Faible"
60 |
61 | VeryLow ->
62 | "Très faible"
63 |
64 | NotApplicable ->
65 | "Non applicable"
66 |
67 |
68 | toString : MakingComplexity -> String
69 | toString makingComplexity =
70 | case makingComplexity of
71 | VeryHigh ->
72 | "very-high"
73 |
74 | High ->
75 | "high"
76 |
77 | Medium ->
78 | "medium"
79 |
80 | Low ->
81 | "low"
82 |
83 | VeryLow ->
84 | "very-low"
85 |
86 | NotApplicable ->
87 | "non-applicable"
88 |
89 |
90 | fromString : String -> Result String MakingComplexity
91 | fromString str =
92 | case str of
93 | "very-high" ->
94 | Ok VeryHigh
95 |
96 | "high" ->
97 | Ok High
98 |
99 | "medium" ->
100 | Ok Medium
101 |
102 | "low" ->
103 | Ok Low
104 |
105 | "very-low" ->
106 | Ok VeryLow
107 |
108 | "not-applicable" ->
109 | Ok NotApplicable
110 |
111 | _ ->
112 | Err ("Type de complexité de fabrication inconnu\u{00A0}: " ++ str)
113 |
114 |
115 | decode : Decoder MakingComplexity
116 | decode =
117 | Decode.string
118 | |> Decode.andThen
119 | (\complexityStr ->
120 | DE.fromResult (fromString complexityStr)
121 | )
122 |
--------------------------------------------------------------------------------
/data/common/impacts.py:
--------------------------------------------------------------------------------
1 | impacts = {
2 | "acd": ("Environmental Footprint 3.1 (adapted) patch wtu", "Acidification"),
3 | "ozd": ("Environmental Footprint 3.1 (adapted) patch wtu", "Ozone depletion"),
4 | "cch": ("Environmental Footprint 3.1 (adapted) patch wtu", "Climate change"),
5 | "fwe": (
6 | "Environmental Footprint 3.1 (adapted) patch wtu",
7 | "Eutrophication, freshwater",
8 | ),
9 | "swe": (
10 | "Environmental Footprint 3.1 (adapted) patch wtu",
11 | "Eutrophication, marine",
12 | ),
13 | "tre": (
14 | "Environmental Footprint 3.1 (adapted) patch wtu",
15 | "Eutrophication, terrestrial",
16 | ),
17 | "pco": (
18 | "Environmental Footprint 3.1 (adapted) patch wtu",
19 | "Photochemical ozone formation",
20 | ),
21 | "pma": ("Environmental Footprint 3.1 (adapted) patch wtu", "Particulate matter"),
22 | "ior": (
23 | "Environmental Footprint 3.1 (adapted) patch wtu",
24 | "Ionising radiation",
25 | ),
26 | "fru": (
27 | "Environmental Footprint 3.1 (adapted) patch wtu",
28 | "Resource use, fossils",
29 | ),
30 | "mru": (
31 | "Environmental Footprint 3.1 (adapted) patch wtu",
32 | "Resource use, minerals and metals",
33 | ),
34 | "ldu": (
35 | "Environmental Footprint 3.1 (adapted) patch wtu",
36 | "Land use",
37 | ),
38 | "wtu": (
39 | "Environmental Footprint 3.1 (adapted) patch wtu",
40 | "Water use",
41 | ),
42 | "etf-o1": (
43 | "Environmental Footprint 3.1 (adapted) patch wtu",
44 | "Ecotoxicity, freshwater - organics - p.1",
45 | ),
46 | "etf-o2": (
47 | "Environmental Footprint 3.1 (adapted) patch wtu",
48 | "Ecotoxicity, freshwater - organics - p.2",
49 | ),
50 | "etf-i": (
51 | "Environmental Footprint 3.1 (adapted) patch wtu",
52 | "Ecotoxicity, freshwater - inorganics",
53 | ),
54 | "etf1": (
55 | "Environmental Footprint 3.1 (adapted) patch wtu",
56 | "Ecotoxicity, freshwater - part 1",
57 | ),
58 | "etf2": (
59 | "Environmental Footprint 3.1 (adapted) patch wtu",
60 | "Ecotoxicity, freshwater - part 2",
61 | ),
62 | "htc": (
63 | "Environmental Footprint 3.1 (adapted) patch wtu",
64 | "Human toxicity, cancer",
65 | ),
66 | "htc-o": (
67 | "Environmental Footprint 3.1 (adapted) patch wtu",
68 | "Human toxicity, cancer - organics",
69 | ),
70 | "htc-i": (
71 | "Environmental Footprint 3.1 (adapted) patch wtu",
72 | "Human toxicity, cancer - inorganics",
73 | ),
74 | "htn": (
75 | "Environmental Footprint 3.1 (adapted) patch wtu",
76 | "Human toxicity, non-cancer",
77 | ),
78 | "htn-o": (
79 | "Environmental Footprint 3.1 (adapted) patch wtu",
80 | "Human toxicity, non-cancer - organics",
81 | ),
82 | "htn-i": (
83 | "Environmental Footprint 3.1 (adapted) patch wtu",
84 | "Human toxicity, non-cancer - inorganics",
85 | ),
86 | }
87 |
--------------------------------------------------------------------------------
/tests/Data/DefinitionTest.elm:
--------------------------------------------------------------------------------
1 | module Data.DefinitionTest exposing (..)
2 |
3 | import Data.Impact.Definition as Definition
4 | import Expect
5 | import Set
6 | import Test exposing (..)
7 | import TestUtils exposing (asTest, suiteWithDb)
8 |
9 |
10 | sumDefinitions : Definition.Base Int -> Int
11 | sumDefinitions =
12 | Definition.foldl (\_ a b -> a + b) 0
13 |
14 |
15 | suite : Test
16 | suite =
17 | suiteWithDb "Data.Impact.Definition"
18 | (\{ textileDb } ->
19 | [ Definition.trigrams
20 | |> List.length
21 | |> Expect.equal 21
22 | |> asTest "There are 21 impact trigrams"
23 | , Definition.trigrams
24 | |> List.map ((\trigram -> Definition.get trigram textileDb.impactDefinitions) >> .trigram >> Definition.toString)
25 | |> Set.fromList
26 | |> Set.toList
27 | |> List.length
28 | |> Expect.equal (List.length Definition.trigrams)
29 | |> asTest "There are 21 unique impact definitions and trigrams"
30 | , Definition.trigrams
31 | |> List.map Definition.toString
32 | |> List.filterMap (Definition.toTrigram >> Result.toMaybe)
33 | |> List.length
34 | |> Expect.equal (List.length Definition.trigrams)
35 | |> asTest "There's a string for each trigram and a trigram for each string"
36 | , Definition.init 0
37 | |> sumDefinitions
38 | |> Expect.equal 0
39 | |> asTest "init will set all the fields to the same value"
40 | , Definition.init 0
41 | |> Definition.map (\_ a -> a + 1)
42 | |> Expect.equal (Definition.init 1)
43 | |> asTest "map will apply a function to all the fields"
44 | , Definition.init 0
45 | |> Definition.update Definition.Acd ((+) 1)
46 | |> sumDefinitions
47 | |> Expect.equal 1
48 | |> asTest "update will change only one field"
49 | , Definition.init 0
50 | |> Definition.update Definition.Acd ((+) 1)
51 | |> (\definitions -> (\trigram -> Definition.get trigram definitions) Definition.Acd)
52 | |> Expect.equal 1
53 | |> asTest "get will retrive the value of a field"
54 | , Definition.init 1
55 | |> Definition.filter ((==) Definition.Acd) (always 0)
56 | |> sumDefinitions
57 | |> Expect.equal 1
58 | |> asTest "filter will zero all the values for fields filtered out"
59 | , Definition.toList textileDb.impactDefinitions
60 | |> List.length
61 | |> Expect.equal 21
62 | |> asTest "there are 21 impacts in total"
63 | , Definition.init 1
64 | |> Definition.filter Definition.isAggregate (always 0)
65 | |> sumDefinitions
66 | |> Expect.equal 2
67 | |> asTest "There are exactly two aggregated scores"
68 | ]
69 | )
70 |
--------------------------------------------------------------------------------
/src/Page/Explore/Table.elm:
--------------------------------------------------------------------------------
1 | module Page.Explore.Table exposing
2 | ( Config
3 | , Table
4 | , viewDetails
5 | , viewList
6 | )
7 |
8 | import Data.Scope exposing (Scope)
9 | import Html exposing (..)
10 | import Html.Attributes exposing (..)
11 | import Html.Events exposing (..)
12 | import Route exposing (Route)
13 | import Table as SortableTable
14 | import Views.Table as TableView
15 |
16 |
17 | type alias Table data comparable msg =
18 | { toId : data -> String
19 | , toRoute : data -> Route
20 | , rows :
21 | List
22 | { label : String
23 | , toValue : data -> comparable
24 | , toCell : data -> Html msg
25 | }
26 | }
27 |
28 |
29 | type alias Config data msg =
30 | { toId : data -> String
31 | , toMsg : SortableTable.State -> msg
32 | , columns : List (SortableTable.Column data msg)
33 | , customizations : SortableTable.Customizations data msg
34 | }
35 |
36 |
37 | viewDetails :
38 | Scope
39 | -> ({ detailed : Bool, scope : Scope } -> Table data comparable msg)
40 | -> data
41 | -> Html msg
42 | viewDetails scope createTable item =
43 | TableView.responsiveDefault [ class "view-details" ]
44 | [ createTable { detailed = True, scope = scope }
45 | |> .rows
46 | |> List.map
47 | (\{ label, toCell } ->
48 | tr []
49 | [ th [] [ text label ]
50 | , td [] [ toCell item ]
51 | ]
52 | )
53 | |> tbody []
54 | ]
55 |
56 |
57 | viewList :
58 | (Route -> msg)
59 | -> Config data msg
60 | -> SortableTable.State
61 | -> Scope
62 | -> ({ detailed : Bool, scope : Scope } -> Table data comparable msg)
63 | -> List data
64 | -> Html msg
65 | viewList routeToMsg defaultConfig tableState scope createTable items =
66 | let
67 | { toId, toRoute, rows } =
68 | createTable { detailed = False, scope = scope }
69 |
70 | customizations =
71 | defaultConfig.customizations
72 |
73 | config =
74 | { defaultConfig
75 | | toId = toId
76 | , columns =
77 | rows
78 | |> List.map
79 | (\{ label, toCell, toValue } ->
80 | SortableTable.veryCustomColumn
81 | { name = label
82 | , viewData = \item -> { attributes = [], children = [ toCell item ] }
83 | , sorter = SortableTable.increasingOrDecreasingBy toValue
84 | }
85 | )
86 | , customizations =
87 | { customizations
88 | | rowAttrs = toRoute >> routeToMsg >> onClick >> List.singleton
89 | }
90 | }
91 | |> SortableTable.customConfig
92 | in
93 | div [ class "DatasetTable table-responsive" ]
94 | [ SortableTable.view config tableState items
95 | ]
96 |
--------------------------------------------------------------------------------
/src/Data/Food/Ingredient/Category.elm:
--------------------------------------------------------------------------------
1 | module Data.Food.Ingredient.Category exposing
2 | ( Category(..)
3 | , decode
4 | , fromAnimalOrigin
5 | , toLabel
6 | )
7 |
8 | import Json.Decode as Decode exposing (Decoder)
9 | import Json.Decode.Extra as DE
10 |
11 |
12 | type Category
13 | = AnimalProduct
14 | | Conventional
15 | | DairyProduct
16 | | GrainRaw
17 | | GrainProcessed
18 | | Misc
19 | | NutOilseedRaw
20 | | NutOilseedProcessed
21 | | SpiceCondimentOrAdditive
22 | | VegetableFresh
23 | | VegetableProcessed
24 | | Organic
25 | | BleuBlancCoeur
26 |
27 |
28 | fromAnimalOrigin : List Category -> Bool
29 | fromAnimalOrigin categories =
30 | [ AnimalProduct, DairyProduct ]
31 | |> List.any (\c -> List.member c categories)
32 |
33 |
34 | fromString : String -> Result String Category
35 | fromString str =
36 | case str of
37 | "animal_product" ->
38 | Ok AnimalProduct
39 |
40 | "conventional" ->
41 | Ok Conventional
42 |
43 | "dairy_product" ->
44 | Ok DairyProduct
45 |
46 | "grain_raw" ->
47 | Ok GrainRaw
48 |
49 | "grain_processed" ->
50 | Ok GrainProcessed
51 |
52 | "misc" ->
53 | Ok Misc
54 |
55 | "nut_oilseed_raw" ->
56 | Ok NutOilseedRaw
57 |
58 | "nut_oilseed_processed" ->
59 | Ok NutOilseedProcessed
60 |
61 | "spice_condiment_additive" ->
62 | Ok SpiceCondimentOrAdditive
63 |
64 | "vegetable_fresh" ->
65 | Ok VegetableFresh
66 |
67 | "vegetable_processed" ->
68 | Ok VegetableProcessed
69 |
70 | "organic" ->
71 | Ok Organic
72 |
73 | "bleublanccoeur" ->
74 | Ok BleuBlancCoeur
75 |
76 | _ ->
77 | Err <| "Categorie d'ingrédient invalide : " ++ str
78 |
79 |
80 | toLabel : Category -> String
81 | toLabel category =
82 | case category of
83 | AnimalProduct ->
84 | "Viandes, œufs, poissons, et dérivés"
85 |
86 | Conventional ->
87 | "Conventionnel"
88 |
89 | DairyProduct ->
90 | "Lait et ingrédients laitiers"
91 |
92 | GrainRaw ->
93 | "Céréales brutes"
94 |
95 | GrainProcessed ->
96 | "Céréales transformées"
97 |
98 | Misc ->
99 | "Divers"
100 |
101 | NutOilseedRaw ->
102 | "Fruits à coque et oléoprotéagineux bruts"
103 |
104 | NutOilseedProcessed ->
105 | "Graisses végétales et oléoprotéagineux transformés"
106 |
107 | SpiceCondimentOrAdditive ->
108 | "Condiments, épices, additifs"
109 |
110 | VegetableFresh ->
111 | "Fruits et légumes frais"
112 |
113 | VegetableProcessed ->
114 | "Fruits et légumes transformés"
115 |
116 | Organic ->
117 | "Bio"
118 |
119 | BleuBlancCoeur ->
120 | "Bleu-Blanc-Cœur"
121 |
122 |
123 | decode : Decoder Category
124 | decode =
125 | Decode.string
126 | |> Decode.andThen (fromString >> DE.fromResult)
127 |
--------------------------------------------------------------------------------
/data/textile/activities.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # from pprint import pprint
3 | from bw2data.project import projects
4 | import bw2data
5 | import json
6 |
7 | projects.create_project("textile", activate=True, exist_ok=True)
8 |
9 | with open("../../public/data/textile/processes.json") as f:
10 | processes = json.loads(f.read())
11 | with open("../../public/data/textile/materials.json") as f:
12 | materials = {m["name"]: m for m in json.loads(f.read())}
13 | with open("codes.json") as f:
14 | codes = {c["code"]: c["name"] for c in json.loads(f.read())}
15 |
16 |
17 | # check no missing process
18 | for material in materials.keys():
19 | if material not in [p["name"] for p in processes]:
20 | print(f"missing process: {material}")
21 |
22 | for process in processes:
23 | name = process["name"]
24 | if name in materials:
25 | process.update(materials[name])
26 | if False: # process["step_usage"] == "Energie":
27 | process["source"] = "Ecoinvent 3.9.1"
28 | del process["impacts"]
29 | # ELEC
30 | if name.startswith("Mix électrique réseau"):
31 | (pname, ccode) = name.split(", ", maxsplit=1)
32 | pname = pname.replace(
33 | "Mix électrique réseau", "Market for electricity, medium voltage"
34 | )
35 | name = f"{pname} {codes[ccode]}"
36 | # STEAM
37 | elif name.startswith("Mix Vapeur"):
38 | if name.endswith("FR") or name.endswith("RER"):
39 | name = "heat from steam in chemical industry RER"
40 | elif name.endswith("RSA"):
41 | name = "heat from steam in chemical industry ROW"
42 | else:
43 | assert False
44 | elif name.startswith("Vapeur à partir de gaz naturel"):
45 | if name.endswith("RER"):
46 | name = "market for heat natural gas europe"
47 | elif name.endswith("RSA"):
48 | name = "market for heat natural gas ROW"
49 | else:
50 | assert False
51 | elif name.startswith("Vapeur à partir de fioul") or name.startswith(
52 | "Vapeur à partir de charbon"
53 | ):
54 | if name.endswith("RER"):
55 | name = "market for heat other Europe"
56 | elif name.endswith("RSA"):
57 | name = "market for heat other ROW"
58 | else:
59 | assert False
60 |
61 | process["name"] = name
62 | new_process = bw2data.Database("Ecoinvent 3.9.1").search(name)
63 | match len(new_process):
64 | case 0:
65 | print(f"Could not find process {name}")
66 | case _:
67 | process["uuid"] = new_process[0].as_dict()["activity"]
68 | if process["source"] == "Base Impacts":
69 | process["source"] = "Base Impacts 2.01"
70 | if process["source"].startswith("Ecoinvent"):
71 | process["correctif"] = ""
72 |
73 | processes = [
74 | {p[0]: p[1].encode("utf-8") if type(p) is str else p[1] for p in process.items()}
75 | for process in processes
76 | ]
77 |
78 | open("activities.json", "w").write(json.dumps(processes, indent=2, ensure_ascii=False))
79 |
80 | # Mix électrique réseau, AN (antilles néerlandaises)
81 | # Mix électrique réseau, non spécifié
82 |
--------------------------------------------------------------------------------
/src/Data/Textile/Printing.elm:
--------------------------------------------------------------------------------
1 | module Data.Textile.Printing exposing
2 | ( Kind(..)
3 | , Printing
4 | , decode
5 | , defaultRatio
6 | , encode
7 | , fromString
8 | , fromStringParam
9 | , kindLabel
10 | , toFullLabel
11 | , toString
12 | )
13 |
14 | import Data.Split as Split exposing (Split)
15 | import Json.Decode as Decode exposing (Decoder)
16 | import Json.Decode.Extra as DE
17 | import Json.Encode as Encode
18 |
19 |
20 | type Kind
21 | = Pigment
22 | | Substantive
23 |
24 |
25 | type alias Printing =
26 | { kind : Kind
27 | , ratio : Split
28 | }
29 |
30 |
31 | decode : Decoder Printing
32 | decode =
33 | Decode.map2 Printing
34 | (Decode.field "kind" decodeKind)
35 | (Decode.field "ratio" Split.decodeFloat)
36 |
37 |
38 | decodeKind : Decoder Kind
39 | decodeKind =
40 | Decode.string
41 | |> Decode.andThen (fromString >> DE.fromResult)
42 |
43 |
44 | defaultRatio : Split
45 | defaultRatio =
46 | Split.twenty
47 |
48 |
49 | encode : Printing -> Encode.Value
50 | encode v =
51 | Encode.object
52 | [ ( "kind", encodeKind v.kind )
53 | , ( "ratio", Split.encodeFloat v.ratio )
54 | ]
55 |
56 |
57 | encodeKind : Kind -> Encode.Value
58 | encodeKind =
59 | toString >> Encode.string
60 |
61 |
62 | fromStringParam : String -> Result String Printing
63 | fromStringParam string =
64 | let
65 | toRatio s =
66 | case String.toFloat s of
67 | Just float ->
68 | if float > 0 && float <= 1 then
69 | Split.fromFloat float
70 |
71 | else
72 | Err "Le ratio de surface d'impression doit être supérieur à zéro et inférieur à 1."
73 |
74 | Nothing ->
75 | Err <| "Ratio de surface teinte invalide: " ++ s
76 | in
77 | case String.split ";" string of
78 | [ "pigment" ] ->
79 | Ok { kind = Pigment, ratio = defaultRatio }
80 |
81 | [ "substantive" ] ->
82 | Ok { kind = Substantive, ratio = defaultRatio }
83 |
84 | [ "pigment", str ] ->
85 | str |> toRatio |> Result.map (Printing Pigment)
86 |
87 | [ "substantive", str ] ->
88 | str |> toRatio |> Result.map (Printing Substantive)
89 |
90 | _ ->
91 | Err <| "Format de type et surface d'impression invalide: " ++ string
92 |
93 |
94 | fromString : String -> Result String Kind
95 | fromString string =
96 | case string of
97 | "pigment" ->
98 | Ok Pigment
99 |
100 | "substantive" ->
101 | Ok Substantive
102 |
103 | _ ->
104 | Err <| "Type d'impression inconnu: " ++ string
105 |
106 |
107 | kindLabel : Kind -> String
108 | kindLabel kind =
109 | case kind of
110 | Pigment ->
111 | "Pigmentaire"
112 |
113 | Substantive ->
114 | "Fixé-lavé"
115 |
116 |
117 | toFullLabel : Printing -> String
118 | toFullLabel { kind, ratio } =
119 | kindLabel kind ++ " (" ++ Split.toPercentString ratio ++ "%)"
120 |
121 |
122 | toString : Kind -> String
123 | toString printing =
124 | case printing of
125 | Pigment ->
126 | "pigment"
127 |
128 | Substantive ->
129 | "substantive"
130 |
--------------------------------------------------------------------------------
/tests/Views/FormatTest.elm:
--------------------------------------------------------------------------------
1 | module Views.FormatTest exposing (..)
2 |
3 | import Data.Split as Split
4 | import Expect
5 | import Html exposing (text)
6 | import Test exposing (..)
7 | import TestUtils exposing (asTest)
8 | import Views.Format as Format
9 |
10 |
11 | suite : Test
12 | suite =
13 | describe "Views.Format"
14 | [ describe "Format.formatFloat"
15 | [ 0
16 | |> Format.formatFloat 99
17 | |> Expect.equal "0"
18 | |> asTest "should format zero"
19 | , 5
20 | |> Format.formatFloat 2
21 | |> Expect.equal "5,00"
22 | |> asTest "should not format an int rendering a specific number of 0 decimals"
23 | , 5.02
24 | |> Format.formatFloat 2
25 | |> Expect.equal "5,02"
26 | |> asTest "should not format a float rounding it at a specific number of decimals"
27 | , 0.502
28 | |> Format.formatFloat 2
29 | |> Expect.equal "0,50"
30 | |> asTest "should not format a float < 1 rounding it at a specific number of decimals"
31 | , 0.0502
32 | |> Format.formatFloat 2
33 | |> Expect.equal "0,05"
34 | |> asTest "should not format a float < 0.1 rounding it at a specific number of decimals"
35 | , 0.00502
36 | |> Format.formatFloat 2
37 | |> Expect.equal "5,02e-3"
38 | |> asTest "should format a float < 0.01 rounding it at a specific number of decimals"
39 | , 0.000502
40 | |> Format.formatFloat 2
41 | |> Expect.equal "5,02e-4"
42 | |> asTest "should format a float < 0.001 in scientific notation (E-3)"
43 | , 0.000000502
44 | |> Format.formatFloat 2
45 | |> Expect.equal "5,02e-7"
46 | |> asTest "should format a float < 0.000001 in scientific notation (E-6)"
47 | , 0.000000000502
48 | |> Format.formatFloat 2
49 | |> Expect.equal "5,02e-10"
50 | |> asTest "should format a float < 0.000000001 in scientific notation (E-9)"
51 | , -5.02
52 | |> Format.formatFloat 2
53 | |> Expect.equal "-5,02"
54 | |> asTest "should not format a negative float in scientific notation"
55 | , -0.000000000502
56 | |> Format.formatFloat 2
57 | |> Expect.equal "-5,02e-10"
58 | |> asTest "should format a negative float in scientific notation"
59 | , 105
60 | |> Format.formatFloat 2
61 | |> Expect.equal "105"
62 | |> asTest "should not format a number > 100 to provided decimal precision"
63 | ]
64 | , describe "Format.percentage"
65 | [ 0.12
66 | |> Split.fromFloat
67 | |> Result.map Format.splitAsPercentage
68 | |> Expect.equal (Ok (text "12\u{202F}%"))
69 | |> asTest "should properly format a Split as percentage"
70 | , 0.12
71 | |> Split.fromFloat
72 | |> Result.map (Format.splitAsFloat 1)
73 | |> Expect.equal (Ok (text "0,1"))
74 | |> asTest "should properly format a Split as float"
75 | ]
76 | ]
77 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import { Elm } from "./src/Main.elm";
2 | import * as Sentry from "@sentry/browser";
3 | import { BrowserTracing } from "@sentry/browser";
4 | import Charts from "./lib/charts";
5 |
6 | // Sentry
7 | if (process.env.SENTRY_DSN) {
8 | Sentry.init({
9 | dsn: process.env.SENTRY_DSN,
10 | integrations: [new BrowserTracing()],
11 | tracesSampleRate: 0,
12 | allowUrls: [
13 | /^https:\/\/ecobalyse\.beta\.gouv\.fr/,
14 | /^https:\/\/ecobalyse\.osc-fr1\.scalingo\.io/,
15 | /^https:\/\/ecobalyse-pr(\d+)\.osc-fr1\.scalingo\.io/,
16 | ],
17 | ignoreErrors: [
18 | // Most often due to DOM-aggressive browser extensions
19 | /_VirtualDom_applyPatch/,
20 | ],
21 | });
22 | }
23 |
24 | function loadScript(scriptUrl) {
25 | var d = document,
26 | g = d.createElement("script"),
27 | s = d.getElementsByTagName("script")[0];
28 | g.async = true;
29 | g.src = scriptUrl;
30 | s.parentNode.insertBefore(g, s);
31 | }
32 |
33 | // The localStorage key to use to store serialized session data
34 | const storeKey = "store";
35 |
36 | const app = Elm.Main.init({
37 | flags: {
38 | clientUrl: location.origin + location.pathname,
39 | rawStore: localStorage[storeKey] || "",
40 | matomo: {
41 | host: process.env.MATOMO_HOST || "",
42 | siteId: process.env.MATOMO_SITE_ID || "",
43 | },
44 | },
45 | });
46 |
47 | app.ports.copyToClipboard.subscribe((text) => {
48 | navigator.clipboard.writeText(text).then(
49 | function () {},
50 | function (err) {
51 | alert(
52 | `Votre navigateur ne supporte pas la copie automatique;
53 | vous pouvez copier l'adresse manuellement`,
54 | );
55 | },
56 | );
57 | });
58 |
59 | app.ports.appStarted.subscribe(() => {
60 | var _paq = (window._paq = window._paq || []);
61 | _paq.push(["trackPageView"]);
62 | _paq.push(["enableLinkTracking"]);
63 | var u = `https://${process.env.MATOMO_HOST}/`;
64 | _paq.push(["setTrackerUrl", u + "matomo.php"]);
65 | _paq.push(["disableCookies"]);
66 | _paq.push(["setSiteId", process.env.MATOMO_SITE_ID]);
67 | loadScript(u + "matomo.js");
68 | });
69 |
70 | app.ports.loadRapidoc.subscribe((rapidocScriptUrl) => {
71 | // load the rapi-doc script if the component hasn't be registered yet
72 | if (!customElements.get("rapi-doc")) {
73 | loadScript(rapidocScriptUrl);
74 | }
75 | });
76 |
77 | app.ports.saveStore.subscribe((rawStore) => {
78 | localStorage[storeKey] = rawStore;
79 | });
80 |
81 | app.ports.addBodyClass.subscribe((cls) => {
82 | document.body.classList.add(cls);
83 | });
84 |
85 | app.ports.removeBodyClass.subscribe((cls) => {
86 | document.body.classList.remove(cls);
87 | });
88 |
89 | app.ports.scrollTo.subscribe((pos) => {
90 | window.scrollTo(pos.x, pos.y);
91 | });
92 |
93 | app.ports.scrollIntoView.subscribe((id) => {
94 | let node = document.getElementById(id);
95 | node?.scrollIntoView({ behavior: "smooth" });
96 | });
97 |
98 | // Ensure session is refreshed when it changes in another tab/window
99 | window.addEventListener(
100 | "storage",
101 | (event) => {
102 | if (event.storageArea === localStorage && event.key === storeKey) {
103 | app.ports.storeChanged.send(event.newValue);
104 | }
105 | },
106 | false,
107 | );
108 |
109 | // Register custom chart elements
110 | Charts.registerElements();
111 |
--------------------------------------------------------------------------------
/src/Data/Textile/Material/Origin.elm:
--------------------------------------------------------------------------------
1 | module Data.Textile.Material.Origin exposing
2 | ( Origin(..)
3 | , decode
4 | , isSynthetic
5 | , threadProcess
6 | , toLabel
7 | , toMicrofibersComplement
8 | , toString
9 | )
10 |
11 | import Data.Unit as Unit
12 | import Json.Decode as Decode exposing (Decoder)
13 | import Json.Decode.Extra as DE
14 |
15 |
16 | type Origin
17 | = ArtificialFromInorganic
18 | | ArtificialFromOrganic
19 | | NaturalFromAnimal
20 | | NaturalFromVegetal
21 | | Synthetic
22 |
23 |
24 | decode : Decoder Origin
25 | decode =
26 | Decode.string
27 | |> Decode.andThen (fromString >> DE.fromResult)
28 |
29 |
30 | fromString : String -> Result String Origin
31 | fromString origin =
32 | case origin of
33 | "ArtificialFromInorganic" ->
34 | Ok ArtificialFromInorganic
35 |
36 | "ArtificialFromOrganic" ->
37 | Ok ArtificialFromOrganic
38 |
39 | "NaturalFromAnimal" ->
40 | Ok NaturalFromAnimal
41 |
42 | "NaturalFromVegetal" ->
43 | Ok NaturalFromVegetal
44 |
45 | "Synthetic" ->
46 | Ok Synthetic
47 |
48 | _ ->
49 | Err <| "Origine inconnue: " ++ origin
50 |
51 |
52 | isSynthetic : Origin -> Bool
53 | isSynthetic origin =
54 | origin == Synthetic
55 |
56 |
57 | toMicrofibersComplement : Origin -> Unit.Impact
58 | toMicrofibersComplement origin =
59 | -- see https://fabrique-numerique.gitbook.io/ecobalyse/textile/limites-methodologiques/old/microfibres#calcul-du-complement-microfibres
60 | -- Notes:
61 | -- - this is a malus expressed as a negative µPts/kg impact
62 | -- - the float value corresponds to Ref(f) * 1000 to ease applying the formula
63 | case origin of
64 | ArtificialFromInorganic ->
65 | Unit.impact -850
66 |
67 | ArtificialFromOrganic ->
68 | Unit.impact -360
69 |
70 | NaturalFromAnimal ->
71 | Unit.impact -570
72 |
73 | NaturalFromVegetal ->
74 | Unit.impact -420
75 |
76 | Synthetic ->
77 | Unit.impact -790
78 |
79 |
80 | toLabel : Origin -> String
81 | toLabel origin =
82 | case origin of
83 | ArtificialFromInorganic ->
84 | "Matière artificielle d'origine inorganique"
85 |
86 | ArtificialFromOrganic ->
87 | "Matière artificielle d'origine organique"
88 |
89 | NaturalFromAnimal ->
90 | "Matière naturelle d'origine animale"
91 |
92 | NaturalFromVegetal ->
93 | "Matière naturelle d'origine végétale"
94 |
95 | Synthetic ->
96 | "Matière synthétique"
97 |
98 |
99 | toString : Origin -> String
100 | toString origin =
101 | case origin of
102 | ArtificialFromInorganic ->
103 | "ArtificialFromInorganic"
104 |
105 | ArtificialFromOrganic ->
106 | "ArtificialFromOrganic"
107 |
108 | NaturalFromAnimal ->
109 | "NaturalFromAnimal"
110 |
111 | NaturalFromVegetal ->
112 | "NaturalFromVegetal"
113 |
114 | Synthetic ->
115 | "Synthetic"
116 |
117 |
118 | threadProcess : Origin -> String
119 | threadProcess origin =
120 | case origin of
121 | Synthetic ->
122 | "Filage"
123 |
124 | _ ->
125 | "Filature"
126 |
--------------------------------------------------------------------------------
/src/Views/AutocompleteSelector.elm:
--------------------------------------------------------------------------------
1 | module Views.AutocompleteSelector exposing (Config, view)
2 |
3 | import Autocomplete exposing (Autocomplete)
4 | import Autocomplete.View as AutocompleteView
5 | import Html exposing (..)
6 | import Html.Attributes exposing (..)
7 | import Views.Modal as ModalView
8 |
9 |
10 | type alias Config element msg =
11 | { autocompleteState : Autocomplete element
12 | , closeModal : msg
13 | , noOp : msg
14 | , onAutocomplete : Autocomplete.Msg element -> msg
15 | , onAutocompleteSelect : msg
16 | , placeholderText : String
17 | , title : String
18 | , toLabel : element -> String
19 | , toCategory : element -> String
20 | }
21 |
22 |
23 | view : Config element msg -> Html msg
24 | view ({ autocompleteState, closeModal, noOp, onAutocomplete, onAutocompleteSelect, placeholderText, title } as config) =
25 | ModalView.view
26 | { size = ModalView.Large
27 | , close = closeModal
28 | , noOp = noOp
29 | , title = title
30 | , subTitle = Nothing
31 | , formAction = Nothing
32 | , content =
33 | let
34 | { query, choices, selectedIndex } =
35 | Autocomplete.viewState autocompleteState
36 |
37 | { inputEvents, choiceEvents } =
38 | AutocompleteView.events
39 | { onSelect = onAutocompleteSelect
40 | , mapHtml = onAutocomplete
41 | }
42 | in
43 | [ input
44 | (inputEvents
45 | ++ [ type_ "search"
46 | , id "element-search"
47 | , class "form-control"
48 | , autocomplete False
49 | , attribute "role" "combobox"
50 | , attribute "aria-autocomplete" "list"
51 | , attribute "aria-owns" "element-autocomplete-choices"
52 | , placeholder placeholderText
53 | , value query
54 | ]
55 | )
56 | []
57 | , choices
58 | |> List.indexedMap (renderChoice config choiceEvents selectedIndex)
59 | |> div [ class "ElementAutocomplete", id "element-autocomplete-choices" ]
60 | ]
61 | , footer = []
62 | }
63 |
64 |
65 | renderChoice : Config element msg -> (Int -> List (Attribute msg)) -> Maybe Int -> Int -> element -> Html msg
66 | renderChoice { toLabel, toCategory } events selectedIndex_ index element =
67 | let
68 | selected =
69 | Autocomplete.isSelected selectedIndex_ index
70 | in
71 | button
72 | (events index
73 | ++ [ class "AutocompleteChoice"
74 | , class "d-flex justify-content-between align-items-center gap-1 w-100"
75 | , class "btn btn-outline-primary border-0 border-bottom text-start no-outline"
76 | , classList [ ( "btn-primary selected", selected ) ]
77 | , attribute "role" "option"
78 | , attribute "aria-selected"
79 | (if selected then
80 | "true"
81 |
82 | else
83 | "false"
84 | )
85 | ]
86 | )
87 | [ span [ class "text-nowrap" ] [ text <| toLabel element ]
88 | , span [ class "text-muted fs-8 text-truncate" ]
89 | [ text <| toCategory element ]
90 | ]
91 |
--------------------------------------------------------------------------------
/src/Views/Modal.elm:
--------------------------------------------------------------------------------
1 | module Views.Modal exposing (Size(..), view)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 | import Html.Events exposing (..)
6 | import Json.Decode as Decode
7 |
8 |
9 | type alias Config msg =
10 | { size : Size
11 | , close : msg
12 | , noOp : msg
13 | , title : String
14 | , subTitle : Maybe String
15 | , content : List (Html msg)
16 | , footer : List (Html msg)
17 | , formAction : Maybe msg
18 | }
19 |
20 |
21 | type Size
22 | = ExtraLarge
23 | | Large
24 | | Small
25 | | Standard
26 |
27 |
28 | view : Config msg -> Html msg
29 | view config =
30 | let
31 | modalContentAttrs =
32 | [ class "modal-content"
33 | , custom "mouseup"
34 | (Decode.succeed
35 | { message = config.noOp
36 | , stopPropagation = True
37 | , preventDefault = True
38 | }
39 | )
40 | ]
41 |
42 | modalContentTag =
43 | case config.formAction of
44 | Just msg ->
45 | Html.form (modalContentAttrs ++ [ onSubmit msg ])
46 |
47 | Nothing ->
48 | div modalContentAttrs
49 | in
50 | div [ class "Modal" ]
51 | [ div
52 | [ class "modal d-block fade show"
53 | , attribute "tabindex" "-1"
54 | , attribute "aria-modal" "true"
55 | , attribute "role" "dialog"
56 | , custom "mouseup"
57 | (Decode.succeed
58 | { message = config.close
59 | , stopPropagation = True
60 | , preventDefault = True
61 | }
62 | )
63 | ]
64 | [ div
65 | [ class "modal-dialog modal-dialog-centered modal-dialog-scrollable"
66 | , classList
67 | [ ( "modal-xl", config.size == ExtraLarge )
68 | , ( "modal-lg", config.size == Large )
69 | , ( "modal-sm", config.size == Small )
70 | ]
71 | , attribute "aria-modal" "true"
72 | ]
73 | [ modalContentTag
74 | [ div [ class "modal-header" ]
75 | [ h5 [ class "modal-title lh-sm" ]
76 | [ span [ class "me-2", attribute "aria-hidden" "true" ] [ text "→" ]
77 | , text config.title
78 | , case config.subTitle of
79 | Just subTitle ->
80 | small [ class "text-muted fs-7 fw-normal ps-2" ] [ text subTitle ]
81 |
82 | Nothing ->
83 | text ""
84 | ]
85 | , button
86 | [ type_ "button"
87 | , class "btn-close"
88 | , onClick config.close
89 | , attribute "aria-label" "Fermer"
90 | ]
91 | []
92 | ]
93 | , config.content
94 | |> div [ class "modal-body no-scroll-chaining p-0" ]
95 | , if config.footer /= [] then
96 | div [ class "modal-footer bg-light" ] config.footer
97 |
98 | else
99 | text ""
100 | ]
101 | ]
102 | ]
103 | , div [ class "modal-backdrop fade show" ] []
104 | ]
105 |
--------------------------------------------------------------------------------
/src/Views/Alert.elm:
--------------------------------------------------------------------------------
1 | module Views.Alert exposing
2 | ( Level(..)
3 | , httpError
4 | , preformatted
5 | , simple
6 | )
7 |
8 | import Data.Env as Env
9 | import Html exposing (..)
10 | import Html.Attributes exposing (..)
11 | import Html.Events exposing (..)
12 | import Http
13 | import Request.Common as HttpCommon
14 | import Views.Icon as Icon
15 |
16 |
17 | type alias Config msg =
18 | { level : Level
19 | , close : Maybe msg
20 | , title : Maybe String
21 | , content : List (Html msg)
22 | }
23 |
24 |
25 | type Level
26 | = Danger
27 | | Info
28 |
29 |
30 | icon : Level -> Html msg
31 | icon level =
32 | case level of
33 | Danger ->
34 | span [ class "me-1" ] [ Icon.warning ]
35 |
36 | Info ->
37 | span [ class "me-1" ] [ Icon.info ]
38 |
39 |
40 | httpError : Http.Error -> Html msg
41 | httpError error =
42 | simple
43 | { title = Just "Erreur de chargement des données"
44 | , close = Nothing
45 | , level = Info
46 | , content =
47 | case error |> HttpCommon.errorToString |> String.lines of
48 | [] ->
49 | []
50 |
51 | [ line ] ->
52 | [ text line ]
53 |
54 | firstLine :: rest ->
55 | [ div []
56 | [ p [ class "mb-2" ] [ text "Une erreur serveur a été rencontrée\u{00A0}:" ]
57 | , pre [ class "mb-1" ] [ text firstLine ]
58 | , details [ class "mb-2" ]
59 | [ summary [] [ text "Afficher les détails de l'erreur" ]
60 | , pre [ class "mt-1" ]
61 | [ rest |> String.join "\n" |> String.trim |> text ]
62 | ]
63 | , a
64 | [ class "btn btn-primary"
65 | , href
66 | ("mailto:"
67 | ++ Env.contactEmail
68 | ++ "?Subject=[Ecobalyse]+Erreur+rencontrée&Body="
69 | ++ HttpCommon.errorToString error
70 | )
71 | ]
72 | [ text "Envoyer un rapport d'incident" ]
73 | ]
74 | ]
75 | }
76 |
77 |
78 | preformatted : Config msg -> Html msg
79 | preformatted config =
80 | simple { config | content = [ pre [ class "fs-7 mb-0" ] config.content ] }
81 |
82 |
83 | simple : Config msg -> Html msg
84 | simple { level, content, title, close } =
85 | div
86 | [ class <| "alert alert-" ++ levelToClass level
87 | , classList [ ( "alert-dismissible", close /= Nothing ) ]
88 | ]
89 | [ case title of
90 | Just title_ ->
91 | h5 [ class "alert-heading d-flex align-items-center" ]
92 | [ icon level, text title_ ]
93 |
94 | Nothing ->
95 | text ""
96 | , div [] content
97 | , case close of
98 | Just closeMsg ->
99 | button
100 | [ type_ "button"
101 | , class "btn-close"
102 | , attribute "aria-label" "Fermer"
103 | , attribute "data-bs-dismiss" "alert"
104 | , onClick closeMsg
105 | ]
106 | []
107 |
108 | Nothing ->
109 | text ""
110 | ]
111 |
112 |
113 | levelToClass : Level -> String
114 | levelToClass level =
115 | case level of
116 | Danger ->
117 | "danger"
118 |
119 | Info ->
120 | "info"
121 |
--------------------------------------------------------------------------------
/src/Data/Bookmark.elm:
--------------------------------------------------------------------------------
1 | module Data.Bookmark exposing
2 | ( Bookmark
3 | , Query(..)
4 | , decode
5 | , encode
6 | , findByFoodQuery
7 | , findByTextileQuery
8 | , isFood
9 | , isTextile
10 | , sort
11 | , toId
12 | , toQueryDescription
13 | )
14 |
15 | import Data.Food.Db as FoodDb
16 | import Data.Food.Query as FoodQuery
17 | import Data.Food.Recipe as Recipe
18 | import Data.Scope as Scope exposing (Scope)
19 | import Data.Textile.Db as TextileDb
20 | import Data.Textile.Inputs as TextileQuery
21 | import Json.Decode as Decode exposing (Decoder)
22 | import Json.Encode as Encode
23 | import Time exposing (Posix)
24 |
25 |
26 | type alias Bookmark =
27 | { name : String
28 | , created : Posix
29 | , query : Query
30 | }
31 |
32 |
33 | type Query
34 | = Food FoodQuery.Query
35 | | Textile TextileQuery.Query
36 |
37 |
38 | decode : Decoder Bookmark
39 | decode =
40 | Decode.map3 Bookmark
41 | (Decode.field "name" Decode.string)
42 | (Decode.field "created" (Decode.map Time.millisToPosix Decode.int))
43 | (Decode.field "query" decodeQuery)
44 |
45 |
46 | decodeQuery : Decoder Query
47 | decodeQuery =
48 | Decode.oneOf
49 | [ Decode.map Food FoodQuery.decode
50 | , Decode.map Textile TextileQuery.decodeQuery
51 | ]
52 |
53 |
54 | encode : Bookmark -> Encode.Value
55 | encode v =
56 | Encode.object
57 | [ ( "name", Encode.string v.name )
58 | , ( "created", Encode.int <| Time.posixToMillis v.created )
59 | , ( "query", encodeQuery v.query )
60 | ]
61 |
62 |
63 | encodeQuery : Query -> Encode.Value
64 | encodeQuery v =
65 | case v of
66 | Food query ->
67 | FoodQuery.encode query
68 |
69 | Textile query ->
70 | TextileQuery.encodeQuery query
71 |
72 |
73 | isFood : Bookmark -> Bool
74 | isFood { query } =
75 | case query of
76 | Food _ ->
77 | True
78 |
79 | _ ->
80 | False
81 |
82 |
83 | isTextile : Bookmark -> Bool
84 | isTextile { query } =
85 | case query of
86 | Textile _ ->
87 | True
88 |
89 | _ ->
90 | False
91 |
92 |
93 | findByQuery : Query -> List Bookmark -> Maybe Bookmark
94 | findByQuery query =
95 | List.filter (.query >> (==) query)
96 | >> List.head
97 |
98 |
99 | findByFoodQuery : FoodQuery.Query -> List Bookmark -> Maybe Bookmark
100 | findByFoodQuery foodQuery =
101 | findByQuery (Food foodQuery)
102 |
103 |
104 | findByTextileQuery : TextileQuery.Query -> List Bookmark -> Maybe Bookmark
105 | findByTextileQuery textileQuery =
106 | findByQuery (Textile textileQuery)
107 |
108 |
109 | scope : Bookmark -> Scope
110 | scope bookmark =
111 | case bookmark.query of
112 | Food _ ->
113 | Scope.Food
114 |
115 | Textile _ ->
116 | Scope.Textile
117 |
118 |
119 | sort : List Bookmark -> List Bookmark
120 | sort =
121 | List.sortBy (.created >> Time.posixToMillis) >> List.reverse
122 |
123 |
124 | toId : Bookmark -> String
125 | toId bookmark =
126 | Scope.toString (scope bookmark) ++ ":" ++ bookmark.name
127 |
128 |
129 | toQueryDescription : { foodDb : FoodDb.Db, textileDb : TextileDb.Db } -> Bookmark -> String
130 | toQueryDescription { foodDb, textileDb } bookmark =
131 | case bookmark.query of
132 | Food foodQuery ->
133 | foodQuery
134 | |> Recipe.fromQuery foodDb
135 | |> Result.map Recipe.toString
136 | |> Result.withDefault bookmark.name
137 |
138 | Textile textileQuery ->
139 | textileQuery
140 | |> TextileQuery.fromQuery textileDb
141 | |> Result.map TextileQuery.toString
142 | |> Result.withDefault bookmark.name
143 |
--------------------------------------------------------------------------------
/src/Views/Impact.elm:
--------------------------------------------------------------------------------
1 | module Views.Impact exposing
2 | ( impactQuality
3 | , selector
4 | )
5 |
6 | import Data.Gitbook as Gitbook
7 | import Data.Impact.Definition as Definition exposing (Definitions)
8 | import Html exposing (..)
9 | import Html.Attributes as Attr exposing (..)
10 | import Html.Events exposing (..)
11 | import Views.Button as Button
12 | import Views.Icon as Icon
13 |
14 |
15 | qualityDocumentationUrl : String
16 | qualityDocumentationUrl =
17 | Gitbook.publicUrlFromPath Gitbook.ImpactQuality
18 |
19 |
20 | impactQuality : Definition.Quality -> List (Html msg)
21 | impactQuality quality =
22 | let
23 | maybeInfo =
24 | case quality of
25 | Definition.NotFinished ->
26 | Just
27 | { cls = "btn-danger"
28 | , icon = Icon.build
29 | , label = "N/A"
30 | , help = "Impact en cours de construction"
31 | }
32 |
33 | Definition.GoodQuality ->
34 | Just
35 | { cls = "btn-success"
36 | , icon = Icon.checkCircle
37 | , label = "I"
38 | , help = "Qualité satisfaisante"
39 | }
40 |
41 | Definition.AverageQuality ->
42 | Just
43 | { cls = "bg-info text-white"
44 | , icon = Icon.info
45 | , label = "II"
46 | , help = "Qualité satisfaisante mais nécessitant des améliorations"
47 | }
48 |
49 | Definition.BadQuality ->
50 | Just
51 | { cls = "btn-warning"
52 | , icon = Icon.warning
53 | , label = "III"
54 | , help = "Donnée incomplète à utiliser avec prudence"
55 | }
56 |
57 | Definition.UnknownQuality ->
58 | Nothing
59 | in
60 | case maybeInfo of
61 | Just { cls, icon, label, help } ->
62 | [ a
63 | [ class <| Button.pillClasses ++ " fs-7 py-0 " ++ cls
64 | , target "_blank"
65 | , href qualityDocumentationUrl
66 | , title help
67 | ]
68 | [ icon
69 | , text "Qualité\u{00A0}: "
70 | , strong [] [ text label ]
71 | ]
72 | ]
73 |
74 | Nothing ->
75 | []
76 |
77 |
78 | type alias SelectorConfig msg =
79 | { selectedImpact : Definition.Trigram
80 | , switchImpact : Result String Definition.Trigram -> msg
81 | }
82 |
83 |
84 | selector : Definitions -> SelectorConfig msg -> Html msg
85 | selector definitions { selectedImpact, switchImpact } =
86 | let
87 | toOption ({ trigram, label } as impact) =
88 | option
89 | [ Attr.selected (selectedImpact == impact.trigram)
90 | , value <| Definition.toString trigram
91 | ]
92 | [ text label ]
93 | in
94 | div [ class "ImpactSelector input-group" ]
95 | [ select
96 | [ class "form-select"
97 | , onInput (Definition.toTrigram >> switchImpact)
98 | ]
99 | [ Definition.toList definitions
100 | |> List.filter (.trigram >> Definition.isAggregate)
101 | |> List.map toOption
102 | |> optgroup [ attribute "label" "Impacts agrégés" ]
103 | , Definition.toList definitions
104 | |> List.filter (.trigram >> Definition.isAggregate >> not)
105 | |> List.sortBy .label
106 | |> List.map toOption
107 | |> optgroup [ attribute "label" "Impacts détaillés" ]
108 | ]
109 | ]
110 |
--------------------------------------------------------------------------------
/data/import_method.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | from bw2data.project import projects
3 | from zipfile import ZipFile
4 | import bw2data
5 | import bw2io
6 | import functools
7 | import os
8 | import sys
9 | from bw2io.strategies import (
10 | drop_unspecified_subcategories,
11 | # fix_localized_water_flows,
12 | link_iterable_by_fields,
13 | match_subcategories,
14 | migrate_exchanges,
15 | normalize_biosphere_categories,
16 | normalize_biosphere_names,
17 | normalize_simapro_biosphere_categories,
18 | normalize_simapro_biosphere_names,
19 | normalize_units,
20 | set_biosphere_type,
21 | )
22 |
23 |
24 | PROJECT = sys.argv[1]
25 | # Agribalyse
26 | BIOSPHERE = "biosphere3"
27 | METHODNAME = "Environmental Footprint 3.1 (adapted) patch wtu" # defined inside the csv
28 | METHODPATH = METHODNAME + ".CSV.zip"
29 |
30 | # excluded strategies and migrations
31 | EXCLUDED_FOOD = [
32 | "normalize_simapro_biosphere_names",
33 | "normalize_biosphere_names",
34 | "fix_localized_water_flows",
35 | "simapro-water",
36 | ]
37 |
38 |
39 | def import_method(datapath=METHODPATH, project=PROJECT, biosphere=BIOSPHERE):
40 | """
41 | Import file at path `datapath` linked to biosphere named `dbname`
42 | """
43 | print(f"### Importing {datapath}...")
44 | projects.set_current(PROJECT)
45 |
46 | # unzip
47 | with ZipFile(datapath) as zf:
48 | print("### Extracting the zip file...")
49 | zf.extractall()
50 | unzipped = datapath[0:-4]
51 |
52 | # projects.create_project(project, activate=True, exist_ok=True)
53 | ef = bw2io.importers.SimaProLCIACSVImporter(
54 | unzipped,
55 | biosphere=biosphere,
56 | normalize_biosphere=False if project == "textile" else True
57 | # normalize_biosphere to align the categories between LCI and LCIA
58 | )
59 | os.unlink(unzipped)
60 | ef.statistics()
61 |
62 | # exclude strategies/migrations in EXCLUDED
63 | if project == "food":
64 | ef.strategies = [
65 | s for s in ef.strategies if not any([e in repr(s) for e in EXCLUDED_FOOD])
66 | ]
67 | if project == "textile":
68 | ef.strategies = [
69 | normalize_units,
70 | set_biosphere_type,
71 | # fix_localized_water_flows, # adding it leads to 60m3
72 | drop_unspecified_subcategories,
73 | functools.partial(normalize_biosphere_categories, lcia=True),
74 | functools.partial(normalize_biosphere_names, lcia=True),
75 | functools.partial(migrate_exchanges, migration="simapro-water"),
76 | normalize_simapro_biosphere_categories,
77 | normalize_simapro_biosphere_names, # removing avoid multiple CFs
78 | functools.partial(
79 | link_iterable_by_fields,
80 | other=(
81 | obj
82 | for obj in bw2data.Database(ef.biosphere_name)
83 | if obj.get("type") == "emission"
84 | ),
85 | # fields=("name", "unit", "categories"),
86 | kind="biosphere",
87 | ),
88 | functools.partial(match_subcategories, biosphere_db_name=ef.biosphere_name),
89 | ]
90 | ef.apply_strategies()
91 |
92 | # ef.write_excel(METHODNAME)
93 | # drop CFs which are not linked to a biosphere substance
94 | ef.drop_unlinked()
95 | ef.write_methods()
96 | print(f"### Finished importing {METHODNAME}")
97 |
98 |
99 | def main():
100 | # Import custom method
101 | projects.set_current(PROJECT)
102 | # projects.create_project(PROJECT, activate=True, exist_ok=True)
103 | bw2data.preferences["biosphere_database"] = BIOSPHERE
104 | bw2io.bw2setup()
105 |
106 | if len([method for method in bw2data.methods if method[0] == METHODNAME]) == 0:
107 | import_method()
108 | else:
109 | print(f"{METHODNAME} already imported")
110 |
111 |
112 | if __name__ == "__main__":
113 | main()
114 |
--------------------------------------------------------------------------------
/src/Data/Split.elm:
--------------------------------------------------------------------------------
1 | module Data.Split exposing
2 | ( Split
3 | , apply
4 | , applyToQuantity
5 | , complement
6 | , decodeFloat
7 | , decodePercent
8 | , divideBy
9 | , encodeFloat
10 | , encodePercent
11 | , fifteen
12 | , fourty
13 | , fromFloat
14 | , fromPercent
15 | , full
16 | , half
17 | , quarter
18 | , tenth
19 | , thirty
20 | , toFloat
21 | , toFloatString
22 | , toPercent
23 | , toPercentString
24 | , twenty
25 | , zero
26 | )
27 |
28 | {-|
29 |
30 | This module manages splits, or "shares", eg: 0.33, or 33%, or a third. Also, the precision will be up to two decimals, so the equivalent of a percent.
31 | 0.121 or 1.119 will both be rounded to 0.12 or 12%.
32 |
33 | -}
34 |
35 | import Json.Decode as Decode exposing (Decoder)
36 | import Json.Encode as Encode
37 | import Quantity exposing (Quantity)
38 |
39 |
40 | type Split
41 | = Split Int
42 |
43 |
44 | zero : Split
45 | zero =
46 | Split 0
47 |
48 |
49 | full : Split
50 | full =
51 | Split 100
52 |
53 |
54 | tenth : Split
55 | tenth =
56 | Split 10
57 |
58 |
59 | fifteen : Split
60 | fifteen =
61 | Split 15
62 |
63 |
64 | twenty : Split
65 | twenty =
66 | Split 20
67 |
68 |
69 | thirty : Split
70 | thirty =
71 | Split 30
72 |
73 |
74 | fourty : Split
75 | fourty =
76 | Split 40
77 |
78 |
79 | half : Split
80 | half =
81 | Split 50
82 |
83 |
84 | quarter : Split
85 | quarter =
86 | Split 25
87 |
88 |
89 | fromFloat : Float -> Result String Split
90 | fromFloat float =
91 | if float < 0 || float > 1 then
92 | Err ("Une part (en nombre flottant) doit être comprise entre 0 et 1 inclus (ici: " ++ String.fromFloat float ++ ")")
93 |
94 | else
95 | float
96 | |> (*) 100
97 | |> round
98 | |> Split
99 | |> Ok
100 |
101 |
102 | fromPercent : Int -> Result String Split
103 | fromPercent int =
104 | if int < 0 || int > 100 then
105 | Err ("Une part (en pourcentage) doit être comprise entre 0 et 100 inclus (ici: " ++ String.fromInt int ++ ")")
106 |
107 | else
108 | Ok (Split int)
109 |
110 |
111 | toFloat : Split -> Float
112 | toFloat (Split int) =
113 | Basics.toFloat int / 100
114 |
115 |
116 | toPercent : Split -> Int
117 | toPercent (Split int) =
118 | int
119 |
120 |
121 | toFloatString : Split -> String
122 | toFloatString =
123 | toFloat >> String.fromFloat
124 |
125 |
126 | toPercentString : Split -> String
127 | toPercentString (Split int) =
128 | String.fromInt int
129 |
130 |
131 | complement : Split -> Split
132 | complement (Split int) =
133 | Split (100 - int)
134 |
135 |
136 | apply : Float -> Split -> Float
137 | apply input split =
138 | toFloat split * input
139 |
140 |
141 | applyToQuantity : Quantity Float units -> Split -> Quantity Float units
142 | applyToQuantity quantity split =
143 | Quantity.multiplyBy (toFloat split) quantity
144 |
145 |
146 | divideBy : Float -> Split -> Float
147 | divideBy input split =
148 | input / toFloat split
149 |
150 |
151 | decodeFloat : Decoder Split
152 | decodeFloat =
153 | Decode.float
154 | |> Decode.map fromFloat
155 | |> Decode.andThen
156 | (\result ->
157 | case result of
158 | Ok split ->
159 | Decode.succeed split
160 |
161 | Err error ->
162 | Decode.fail error
163 | )
164 |
165 |
166 | decodePercent : Decoder Split
167 | decodePercent =
168 | Decode.int
169 | |> Decode.map fromPercent
170 | |> Decode.andThen
171 | (\result ->
172 | case result of
173 | Ok split ->
174 | Decode.succeed split
175 |
176 | Err error ->
177 | Decode.fail error
178 | )
179 |
180 |
181 | encodeFloat : Split -> Encode.Value
182 | encodeFloat =
183 | toFloat >> Encode.float
184 |
185 |
186 | encodePercent : Split -> Encode.Value
187 | encodePercent =
188 | toPercent >> Encode.int
189 |
--------------------------------------------------------------------------------
/src/Views/Table.elm:
--------------------------------------------------------------------------------
1 | module Views.Table exposing
2 | ( percentageTable
3 | , responsiveDefault
4 | )
5 |
6 | import Data.Impact.Definition exposing (Definition)
7 | import Html exposing (..)
8 | import Html.Attributes exposing (..)
9 | import Views.Format as Format
10 |
11 |
12 | type alias DataPoint msg =
13 | { name : String
14 | , value : Float
15 | , entryAttributes : List (Attribute msg)
16 | }
17 |
18 |
19 | responsiveDefault : List (Attribute msg) -> List (Html msg) -> Html msg
20 | responsiveDefault attrs content =
21 | div [ class "DatasetTable table-responsive" ]
22 | [ table
23 | (class "table table-striped table-hover table-responsive mb-0"
24 | :: attrs
25 | )
26 | content
27 | ]
28 |
29 |
30 | percentageTable : Definition -> List (DataPoint msg) -> Html msg
31 | percentageTable impactDefinition data =
32 | let
33 | values =
34 | List.map .value data
35 |
36 | ( total, minimum, maximum ) =
37 | ( List.sum values
38 | , values |> List.maximum |> Maybe.withDefault 0
39 | , values |> List.maximum |> Maybe.withDefault 0
40 | )
41 | in
42 | if total == 0 || maximum == 0 then
43 | text ""
44 |
45 | else
46 | div [ class "table-responsive", style "max-height" "400px" ]
47 | [ table [ class "table table-hover w-100 m-0" ]
48 | [ data
49 | |> List.map
50 | (\{ name, value, entryAttributes } ->
51 | { name = name
52 | , impact = value
53 | , percent = value / total * 100
54 | , width =
55 | if value < 0 then
56 | abs (value / minimum * 100)
57 |
58 | else
59 | value / maximum * 100
60 | , entryAttributes = entryAttributes
61 | }
62 | )
63 | |> List.map
64 | (\{ name, impact, percent, width, entryAttributes } ->
65 | let
66 | entryTitle =
67 | name
68 | ++ ": "
69 | ++ Format.formatFloat 2 percent
70 | ++ "\u{202F}% ("
71 | ++ Format.formatFloat 2 impact
72 | ++ "\u{202F}"
73 | ++ impactDefinition.unit
74 | ++ ")"
75 | in
76 | tr
77 | (title entryTitle
78 | :: entryAttributes
79 | )
80 | [ th [ class "text-truncate fw-normal fs-8", style "max-width" "200px" ] [ text name ]
81 | , td [ class "HorizontalBarChart", style "width" "200px", style "vertical-align" "middle" ]
82 | [ div
83 | [ class "ext"
84 | , classList [ ( "pos", percent >= 0 ), ( "neg", percent < 0 ) ]
85 | ]
86 | [ div
87 | [ class "bar"
88 | , classList [ ( "bg-secondary", percent >= 0 ), ( "bg-success", percent < 0 ) ]
89 | , style "width" (String.fromFloat width ++ "%")
90 | ]
91 | []
92 | ]
93 | ]
94 | , td [ class "text-end text-nowrap fs-8" ]
95 | [ Format.percent percent
96 | ]
97 | ]
98 | )
99 | |> tbody []
100 | ]
101 | ]
102 |
--------------------------------------------------------------------------------
/src/Server/Route.elm:
--------------------------------------------------------------------------------
1 | module Server.Route exposing
2 | ( Route(..)
3 | , endpoint
4 | )
5 |
6 | import Data.Food.Query as BuilderQuery
7 | import Data.Impact as Impact
8 | import Data.Impact.Definition as Definition
9 | import Data.Textile.Inputs as TextileInputs
10 | import Server.Query as Query
11 | import Server.Request exposing (Request)
12 | import Static.Db as StaticDb
13 | import Url
14 | import Url.Parser as Parser exposing ((>), (>), Parser, s)
15 |
16 |
17 | {-| A server request route.
18 |
19 | Note: The API root, serving the OpenAPI documentation, is handled by the
20 | ExpressJS server directly (see server.js).
21 |
22 | -}
23 | type Route
24 | = -- Food Routes
25 | -- GET
26 | -- Food country list
27 | GetFoodCountryList
28 | -- Food ingredient list
29 | | GetFoodIngredientList
30 | -- Food packaging list
31 | | GetFoodPackagingList
32 | -- Food transforms list
33 | | GetFoodTransformList
34 | -- Food recipe builder (GET, query string)
35 | | GetFoodRecipe (Result Query.Errors BuilderQuery.Query)
36 | -- POST
37 | -- Food recipe builder (POST, JSON body)
38 | | PostFoodRecipe
39 | --
40 | -- Textile Routes
41 | -- GET
42 | -- Textile country list
43 | | GetTextileCountryList
44 | -- Textile Material list
45 | | GetTextileMaterialList
46 | -- Textile Product list
47 | | GetTextileProductList
48 | -- Textile Simple version of all impacts (GET, query string)
49 | | GetTextileSimulator (Result Query.Errors TextileInputs.Query)
50 | -- Textile Detailed version for all impacts (GET, query string)
51 | | GetTextileSimulatorDetailed (Result Query.Errors TextileInputs.Query)
52 | -- Textile Simple version for one specific impact (GET, query string)
53 | | GetTextileSimulatorSingle Definition.Trigram (Result Query.Errors TextileInputs.Query)
54 | -- POST
55 | -- Textile Simple version of all impacts (POST, JSON body)
56 | | PostTextileSimulator
57 | -- Textile Detailed version for all impacts (POST, JSON body)
58 | | PostTextileSimulatorDetailed
59 | -- Textile Simple version for one specific impact (POST, JSON bosy)
60 | | PostTextileSimulatorSingle Definition.Trigram
61 |
62 |
63 | parser : StaticDb.Db -> Parser (Route -> a) a
64 | parser { foodDb, textileDb } =
65 | Parser.oneOf
66 | [ -- Food
67 | Parser.map GetFoodCountryList (s "GET" > s "food" > s "countries")
68 | , Parser.map GetFoodIngredientList (s "GET" > s "food" > s "ingredients")
69 | , Parser.map GetFoodTransformList (s "GET" > s "food" > s "transforms")
70 | , Parser.map GetFoodPackagingList (s "GET" > s "food" > s "packagings")
71 | , Parser.map GetFoodRecipe (s "GET" > s "food" > s "recipe" > Query.parseFoodQuery foodDb)
72 | , Parser.map PostFoodRecipe (s "POST" > s "food" > s "recipe")
73 |
74 | -- Textile
75 | , Parser.map GetTextileCountryList (s "GET" > s "textile" > s "countries")
76 | , Parser.map GetTextileMaterialList (s "GET" > s "textile" > s "materials")
77 | , Parser.map GetTextileProductList (s "GET" > s "textile" > s "products")
78 | , Parser.map GetTextileSimulator (s "GET" > s "textile" > s "simulator" > Query.parseTextileQuery textileDb)
79 | , Parser.map GetTextileSimulatorDetailed (s "GET" > s "textile" > s "simulator" > s "detailed" > Query.parseTextileQuery textileDb)
80 | , Parser.map GetTextileSimulatorSingle (s "GET" > s "textile" > s "simulator" > Impact.parseTrigram > Query.parseTextileQuery textileDb)
81 | , Parser.map PostTextileSimulator (s "POST" > s "textile" > s "simulator")
82 | , Parser.map PostTextileSimulatorDetailed (s "POST" > s "textile" > s "simulator" > s "detailed")
83 | , Parser.map PostTextileSimulatorSingle (s "POST" > s "textile" > s "simulator" > Impact.parseTrigram)
84 | ]
85 |
86 |
87 | endpoint : StaticDb.Db -> Request -> Maybe Route
88 | endpoint dbs { method, url } =
89 | -- Notes:
90 | -- - Url.fromString can't build a Url without a fully qualified URL, so as we only have the
91 | -- request path from Express, we build a fake URL with a fake protocol and hostname.
92 | -- - We update the path appending the HTTP method to it, for simpler, cheaper route parsing.
93 | Url.fromString ("http://x/" ++ method ++ url)
94 | |> Maybe.andThen (Parser.parse (parser dbs))
95 |
--------------------------------------------------------------------------------
/src/Data/Textile/Material.elm:
--------------------------------------------------------------------------------
1 | module Data.Textile.Material exposing
2 | ( CFFData
3 | , Id(..)
4 | , Material
5 | , decodeList
6 | , encode
7 | , encodeId
8 | , findById
9 | , getRecyclingData
10 | , idToString
11 | )
12 |
13 | import Data.Country as Country
14 | import Data.Split as Split exposing (Split)
15 | import Data.Textile.Material.Origin as Origin exposing (Origin)
16 | import Data.Textile.Process as Process exposing (Process)
17 | import Json.Decode as Decode exposing (Decoder)
18 | import Json.Decode.Pipeline as JDP
19 | import Json.Encode as Encode
20 |
21 |
22 | type alias Material =
23 | { id : Id
24 | , name : String
25 | , shortName : String
26 | , origin : Origin
27 | , materialProcess : Process
28 | , recycledProcess : Maybe Process
29 | , recycledFrom : Maybe Id
30 | , geographicOrigin : String -- A textual information about the geographic origin of the material
31 | , defaultCountry : Country.Code -- Default country for Material and Spinning steps
32 | , priority : Int -- Used to sort materials
33 | , cffData : Maybe CFFData
34 | }
35 |
36 |
37 | type Id
38 | = Id String
39 |
40 |
41 |
42 | ---- Recycling
43 |
44 |
45 | type alias CFFData =
46 | -- Circular Footprint Formula data
47 | { manufacturerAllocation : Split
48 | , recycledQualityRatio : Split
49 | }
50 |
51 |
52 | getRecyclingData : Material -> List Material -> Maybe ( Material, CFFData )
53 | getRecyclingData material materials =
54 | -- If material is non-recycled, retrieve relevant recycled equivalent material & CFF data
55 | Maybe.map2 Tuple.pair
56 | (material.recycledFrom
57 | |> Maybe.andThen
58 | (\id ->
59 | findById id materials
60 | |> Result.toMaybe
61 | )
62 | )
63 | material.cffData
64 |
65 |
66 |
67 | ---- Helpers
68 |
69 |
70 | findById : Id -> List Material -> Result String Material
71 | findById id =
72 | List.filter (.id >> (==) id)
73 | >> List.head
74 | >> Result.fromMaybe ("Matière non trouvée id=" ++ idToString id ++ ".")
75 |
76 |
77 | decode : List Process -> Decoder Material
78 | decode processes =
79 | Decode.succeed Material
80 | |> JDP.required "id" (Decode.map Id Decode.string)
81 | |> JDP.required "name" Decode.string
82 | |> JDP.required "shortName" Decode.string
83 | |> JDP.required "origin" Origin.decode
84 | |> JDP.required "materialProcessUuid" (Process.decodeFromUuid processes)
85 | |> JDP.required "recycledProcessUuid" (Decode.maybe (Process.decodeFromUuid processes))
86 | |> JDP.required "recycledFrom" (Decode.maybe (Decode.map Id Decode.string))
87 | |> JDP.required "geographicOrigin" Decode.string
88 | |> JDP.required "defaultCountry" (Decode.string |> Decode.map Country.codeFromString)
89 | |> JDP.required "priority" Decode.int
90 | |> JDP.required "cff" (Decode.maybe decodeCFFData)
91 |
92 |
93 | decodeCFFData : Decoder CFFData
94 | decodeCFFData =
95 | Decode.succeed CFFData
96 | |> JDP.required "manufacturerAllocation" Split.decodeFloat
97 | |> JDP.required "recycledQualityRatio" Split.decodeFloat
98 |
99 |
100 | decodeList : List Process -> Decoder (List Material)
101 | decodeList processes =
102 | Decode.list (decode processes)
103 |
104 |
105 | encode : Material -> Encode.Value
106 | encode v =
107 | Encode.object
108 | [ ( "id", encodeId v.id )
109 | , ( "name", v.name |> Encode.string )
110 | , ( "shortName", Encode.string v.shortName )
111 | , ( "origin", v.origin |> Origin.toString |> Encode.string )
112 | , ( "materialProcessUuid", Process.encodeUuid v.materialProcess.uuid )
113 | , ( "recycledProcessUuid"
114 | , v.recycledProcess |> Maybe.map (.uuid >> Process.encodeUuid) |> Maybe.withDefault Encode.null
115 | )
116 | , ( "recycledFrom", v.recycledFrom |> Maybe.map encodeId |> Maybe.withDefault Encode.null )
117 | , ( "geographicOrigin", Encode.string v.geographicOrigin )
118 | , ( "defaultCountry", v.defaultCountry |> Country.codeToString |> Encode.string )
119 | , ( "priority", Encode.int v.priority )
120 | ]
121 |
122 |
123 | encodeId : Id -> Encode.Value
124 | encodeId =
125 | idToString >> Encode.string
126 |
127 |
128 | idToString : Id -> String
129 | idToString (Id string) =
130 | string
131 |
--------------------------------------------------------------------------------
/data/README.md:
--------------------------------------------------------------------------------
1 | Comment générer les données json utilisées par le frontal elm :
2 |
3 | # Avec docker
4 |
5 | - Installez `docker` et `make`
6 | - Si vous êtes sur Mac avec architecture ARM, affectez 6Go de RAM à Docker dans Docker Desktop :
7 | Settings → Ressources → Advanced → Memory = 6G
8 | - Préparez les bases de données à importer, elle ne font pas partie du dépôt :
9 | - Agribalyse : compressé dans un fichier `AGB3.1.1.20230306.CSV.zip` dans ce dossier data/
10 | - Ecoinvent : décompressé dans un dossier `ECOINVENT3.9.1` dans ce même dossier
11 | - Lancez **`make`** ce qui va successivement :
12 | - construire l'image docker
13 | - importer agribalyse et EF 3.1 adapted dans un projet `food` de Brightway
14 | - importer ecoinvent et EF 3.1 adapted dans un projet `textile` de Brightway
15 | - exporter les données json utilisées côté front-end
16 |
17 | Le processus entier prend environ 1h. En cas de problème vous pouvez redémarrer de zéro en faisant
18 | d'abord un `make clean_data` (qui supprime le volume docker).
19 |
20 | ## Autres commandes :
21 |
22 | - `make image` : pour construire l'image docker choisie
23 | - `make import_agribalyse` : pour importer Agribalyse 3.1.1 dans Brightway (projet food).
24 | Assurez-vous d'avoir le fichier `AGB3.1.1.20230306.CSV.zip` dans le dossier `data/`
25 | - `make import_food_method` : pour importer EF 3.1 adapted dans Brightway (projet food).
26 | Assurez-vous d'avoir le fichier `Environmental Footprint 3.1 (adapted).CSV` dans le dossier
27 | `data/`
28 | - `make import_textile_method` : pour importer EF 3.1 adapted dans Brightway (projet textile).
29 | Assurez-vous d'avoir le fichier `Environmental Footprint 3.1 (adapted).CSV` dans le dossier
30 | `data/`
31 | - `make import_ecoinvent` : pour importer Ecoinvent 3.9.1. Brightway (projet textile). Assurez-vous
32 | d'avoir le dossier `ECOINVENT3.9.1/` dans le dossier `data/`
33 | - `make export_food` : pour exporter les json pour le builder alimentaire
34 | - `make export_textile` : pour exporter les json pour le builder textile
35 | - `make delete_textile_method` : pour supprimer la méthode utilisée dans le projet textile
36 | - `make json` : lance toutes les commandes précédentes dans l'ordre
37 | - `make shell` : lance un shell bash à l'intérieur du conteneur
38 | - `make python` : lance un interpréteur Python à l'intérieur du conteneur
39 | - `make jupyter_password` : définit le mot de passe jupyter. Doit être lancé avant son démarrage.
40 | - `make root_shell` : lance un shell root à l'intérieur du conteneur
41 | - `make jupyter_password` : pour définir le mot de passe de Jupyter avant de le lancer
42 | - `make start_notebook` : lance le serveur Jupyter dans le conteneur
43 | - `make stop_notebook` : arrête le serveur Jupyter donc aussi le conteneur
44 | - `make clean_data` : supprime toutes les données (celles de brightway et jupyter mais pas les json
45 | générés)
46 | - `make clean_image` : supprime l'image docker
47 | - `make clean` : lance `clean_data` et `clean_image`
48 |
49 | ## Travailler dans le conteneur :
50 |
51 | Vous pouvez entrer dans le conteneur avec `make shell`.
52 |
53 | Toutes les données du conteneur, notamment celles de Brightway et de Jupyter, sont dans
54 | `/home/jovyan` qui est situé dans un volume docker (`/var/lib/docker/volume/jovyan` sur le _host_).
55 | Le dépôt git ecobalyse se retrouve (via un bind mount) aussi à l'intérieur du conteneur dans
56 | `/home/jovyan/ecobalyse`. Les fichiers json générés arrivent directement sur place au bon endroit
57 | pour être comparées puis commités.
58 |
59 | ## Lancer le serveur Jupyter de dev
60 |
61 | Avant de lancer Jupyter vous pouvez définir son mot de passe avec `make jupyter_password`. Ensuite
62 | vous le démarrez avec `make start_notebook`.
63 |
64 | ## Lancer le serveur Jupyter pour l'éditeur d'ingrédients
65 |
66 | Avant de lancer Jupyter vous pouvez définir son mot de passe avec `make jupyter_password`. Ensuite
67 | vous le démarrez avec `JUPYTER_PORT=8889 make start_notebook`.
68 |
69 | ## Lancer l'explorateur Brightway
70 |
71 | Créez un notebook dans Jupyter puis tapez `import notebooks.explore`, puis shift-Enter
72 |
73 | ## Lancer l'éditeur de procédés/ingrédients
74 |
75 | Créez un notebook dans Jupyter puis tapez `import notebooks.ingredients`, puis shift-Enter
76 |
77 | ## Remarques
78 |
79 | Si l'`export` prend plus de 2 secondes par procédé, c'est un problème d'installation de `pypardiso`
80 | ou de la bibliothèque `mkl` (Math Kernel Library d'Intel) ou une incompatibilité avec l'architecture
81 | CPU utilisée. Dans ce cas c'est le solveur de Scipy qui est utilisé. Il est possible que cela
82 | explique les très légères différences d'arrondi rencontrées dans les résultats.
83 |
--------------------------------------------------------------------------------
/data/bwapi/server.py:
--------------------------------------------------------------------------------
1 | from bw2data.project import projects
2 | from bw2data.utils import get_activity
3 | from fastapi import FastAPI, Request
4 | from fastapi.responses import JSONResponse
5 | from typing import Union
6 | import bw2calc
7 | import bw2data
8 |
9 | api = FastAPI()
10 |
11 | # projects and databases
12 |
13 |
14 | @api.get("/projects", response_class=JSONResponse)
15 | async def projectlist(_: Request):
16 | return list(bw2data.projects)
17 |
18 |
19 | @api.get("/{project}/databases", response_class=JSONResponse)
20 | async def databases(_: Request, project: str):
21 | projects.set_current(project)
22 | return list(bw2data.databases)
23 |
24 |
25 | # search
26 |
27 |
28 | @api.get("/{project}/{dbname}/search/", response_class=JSONResponse)
29 | async def search(
30 | _: Request,
31 | project: str,
32 | dbname: str,
33 | q: Union[str, None],
34 | limit: Union[int, None] = 20,
35 | ):
36 | projects.set_current(project)
37 | return [a.as_dict() for a in bw2data.Database(dbname).search(q, limit=limit)]
38 |
39 |
40 | # activity data and graph
41 |
42 |
43 | @api.get("/{project}/{dbname}/{code}/data", response_class=JSONResponse)
44 | async def data(_: Request, project: str, dbname: str, code: str):
45 | projects.set_current(project)
46 | return get_activity((dbname, code)).as_dict()
47 |
48 |
49 | @api.get("/{project}/{dbname}/{code}/technosphere", response_class=JSONResponse)
50 | async def technosphere(_: Request, project: str, dbname: str, code: str):
51 | projects.set_current(project)
52 | return [
53 | exchange.as_dict() for exchange in get_activity((dbname, code)).technosphere()
54 | ]
55 |
56 |
57 | @api.get("/{project}/{dbname}/{code}/biosphere", response_class=JSONResponse)
58 | async def biosphere(_: Request, project: str, dbname: str, code: str):
59 | projects.set_current(project)
60 | return [exchange.as_dict() for exchange in get_activity((dbname, code)).biosphere()]
61 |
62 |
63 | @api.get("/{project}/{dbname}/{code}/substitution", response_class=JSONResponse)
64 | async def substitution(_: Request, project: str, dbname: str, code: str):
65 | projects.set_current(project)
66 | return [
67 | exchange.as_dict() for exchange in get_activity((dbname, code)).substitution()
68 | ]
69 |
70 |
71 | # methods
72 |
73 |
74 | @api.get("/{project}/methods", response_class=JSONResponse)
75 | async def methods(_: Request, project: str):
76 | projects.set_current(project)
77 | return sorted({m[0] for m in bw2data.methods})
78 |
79 |
80 | @api.get("/{project}/methods/{method}", response_class=JSONResponse)
81 | async def method(_: Request, project: str, method: str):
82 | projects.set_current(project)
83 | return sorted({m for m in bw2data.methods if m[0] == method})
84 |
85 |
86 | @api.get(
87 | "/{project}/methods/{method}/{impact_category:path}",
88 | response_class=JSONResponse,
89 | )
90 | async def impact_category(_: Request, project: str, method: str, impact_category: str):
91 | projects.set_current(project)
92 | try:
93 | return bw2data.methods[(method,) + tuple(impact_category.split("/"))]
94 | except KeyError:
95 | return JSONResponse(
96 | status_code=404, content={"message": "Impact category not found"}
97 | )
98 |
99 |
100 | @api.get(
101 | "/{project}/characterization_factors/{method}/{impact_category:path}",
102 | response_class=JSONResponse,
103 | )
104 | async def characterization_factors(
105 | _: Request, project: str, method: str, impact_category: str
106 | ):
107 | projects.set_current(project)
108 | try:
109 | return bw2data.Method((method,) + tuple(impact_category.split("/"))).load()
110 | except:
111 | return JSONResponse(
112 | status_code=404, content={"message": "Impact category not found"}
113 | )
114 |
115 |
116 | # impacts
117 |
118 |
119 | @api.get("/{project}/{dbname}/{code}/impacts/{method}", response_class=JSONResponse)
120 | async def impacts(_: Request, project: str, dbname: str, code: str, method: str):
121 | projects.set_current(project)
122 | lca = bw2calc.LCA({get_activity((dbname, code)): 1})
123 | lca.lci()
124 | impacts = []
125 | for m in [m for m in list(bw2data.methods) if m[0] == method]:
126 | lca.switch_method(m)
127 | lca.lcia()
128 | impacts.append(
129 | {
130 | "method": m,
131 | "score": lca.score,
132 | "unit": bw2data.methods[m].get("unit", "(no unit)"),
133 | }
134 | )
135 | return impacts
136 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const fs = require("fs");
3 | const express = require("express");
4 | const bodyParser = require("body-parser");
5 | const cors = require("cors");
6 | const yaml = require("js-yaml");
7 | const helmet = require("helmet");
8 | const Sentry = require("@sentry/node");
9 | const { Elm } = require("./server-app");
10 | const lib = require("./lib");
11 |
12 | const app = express(); // web app
13 | const api = express(); // api app
14 | const host = "0.0.0.0";
15 | const port = process.env.PORT || 3000;
16 |
17 | // Env vars
18 | const { SENTRY_DSN, MATOMO_HOST, MATOMO_SITE_ID, MATOMO_TOKEN } = process.env;
19 |
20 | // Matomo
21 | if (process.env.NODE_ENV !== "test" && (!MATOMO_HOST || !MATOMO_SITE_ID || !MATOMO_TOKEN)) {
22 | console.error("Matomo environment variables are missing. Please check the README.");
23 | process.exit(1);
24 | }
25 |
26 | // Sentry
27 | if (SENTRY_DSN) {
28 | Sentry.init({ dsn: SENTRY_DSN, tracesSampleRate: 0.1 });
29 | // Note: Sentry middleware *must* be the very first applied to be effective
30 | app.use(Sentry.Handlers.requestHandler());
31 | }
32 |
33 | // Web
34 |
35 | // Note: helmet middlewares have to be called *after* the Sentry middleware
36 | // but *before* other middlewares to be applied effectively
37 | app.use(
38 | helmet({
39 | crossOriginEmbedderPolicy: false,
40 | hsts: false,
41 | xssFilter: false,
42 | contentSecurityPolicy: {
43 | useDefaults: true,
44 | directives: {
45 | "default-src": [
46 | "'self'",
47 | "https://api.github.com",
48 | "https://raw.githubusercontent.com",
49 | "https://sentry.incubateur.net",
50 | "*.gouv.fr",
51 | ],
52 | "frame-src": ["'self'", `https://${process.env.MATOMO_HOST}`],
53 | "img-src": [
54 | "'self'",
55 | "data:",
56 | "blob:",
57 | "https://avatars.githubusercontent.com/",
58 | "https://raw.githubusercontent.com",
59 | ],
60 | // FIXME: We should be able to remove 'unsafe-inline' as soon as the Matomo
61 | // server sends the appropriate `Access-Control-Allow-Origin` header
62 | // @see https://matomo.org/faq/how-to/faq_18694/
63 | "script-src": ["'self'", "'unsafe-inline'", `https://${process.env.MATOMO_HOST}`],
64 | "object-src": ["blob:"],
65 | },
66 | },
67 | }),
68 | );
69 |
70 | app.use(
71 | express.static("dist", {
72 | setHeaders: (res) => {
73 | // Note: helmet sets this header to `0` by default and doesn't allow overriding
74 | // this value
75 | res.set("X-XSS-Protection", "1; mode=block");
76 | },
77 | }),
78 | );
79 |
80 | // Redirects: Web
81 | app.get("/accessibilite", (_, res) => res.redirect("/#/pages/accessibilité"));
82 | app.get("/mentions-legales", (_, res) => res.redirect("/#/pages/mentions-légales"));
83 | app.get("/stats", (_, res) => res.redirect("/#/stats"));
84 |
85 | // API
86 |
87 | const openApiContents = yaml.load(fs.readFileSync("openapi.yaml"));
88 |
89 | // Matomo
90 | const apiTracker = lib.setupTracker(openApiContents);
91 |
92 | const elmApp = Elm.Server.init();
93 |
94 | elmApp.ports.output.subscribe(({ status, body, jsResponseHandler }) => {
95 | return jsResponseHandler({ status, body });
96 | });
97 |
98 | api.get("/", (req, res) => {
99 | apiTracker.track(200, req);
100 | res.status(200).send(openApiContents);
101 | });
102 |
103 | // Redirects: API
104 | api.get(/^\/countries$/, (_, res) => res.redirect("textile/countries"));
105 | api.get(/^\/materials$/, (_, res) => res.redirect("textile/materials"));
106 | api.get(/^\/products$/, (_, res) => res.redirect("textile/products"));
107 | const cleanRedirect = (url) => (url.startsWith("/") ? url : "");
108 | api.get(/^\/simulator(.*)$/, ({ url }, res) => res.redirect(`/api/textile${cleanRedirect(url)}`));
109 |
110 | // Note: Text/JSON request body parser (JSON is decoded in Elm)
111 | api.all(/(.*)/, bodyParser.json(), (req, res) => {
112 | elmApp.ports.input.send({
113 | method: req.method,
114 | url: req.url,
115 | body: req.body,
116 | jsResponseHandler: ({ status, body }) => {
117 | apiTracker.track(status, req);
118 | res.status(status).send(body);
119 | },
120 | });
121 | });
122 |
123 | api.use(cors()); // Enable CORS for all API requests
124 | app.use("/api", api);
125 |
126 | // Sentry error handler
127 | // Note: *must* be called *before* any other error handler
128 | if (SENTRY_DSN) {
129 | app.use(Sentry.Handlers.errorHandler());
130 | }
131 |
132 | const server = app.listen(port, host, () => {
133 | console.log(`Server listening at http://${host}:${port}`);
134 | });
135 |
136 | module.exports = server;
137 |
--------------------------------------------------------------------------------