├── .dockerignore
├── .env
├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .prettierrc.json
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── VERSION
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── scripts
└── parse-env.sh
├── src
├── config.js
├── index.css
├── index.js
├── scenes
│ └── papers
│ │ ├── Papers.css
│ │ ├── Papers.js
│ │ ├── components
│ │ ├── Header.css
│ │ ├── Header.js
│ │ ├── Panel.css
│ │ ├── Panel.js
│ │ ├── Sidebar.js
│ │ ├── header
│ │ │ ├── HeaderModal.css
│ │ │ └── HeaderModal.js
│ │ └── panel
│ │ │ ├── map
│ │ │ ├── Map.css
│ │ │ ├── Map.js
│ │ │ ├── MapInfoBox.js
│ │ │ ├── MapLayerControl.js
│ │ │ ├── MapSelectPaper.js
│ │ │ ├── MapTileLayer.js
│ │ │ └── events
│ │ │ │ └── MapClickEvent.js
│ │ │ └── search
│ │ │ ├── JargonSearch.js
│ │ │ ├── PaperSearch.js
│ │ │ └── Search.css
│ │ ├── controllers
│ │ ├── backend
│ │ │ ├── JargonSearch.js
│ │ │ └── MetricSearch.js
│ │ └── paperscape
│ │ │ ├── MapConfig.js
│ │ │ ├── MapLabels.js
│ │ │ ├── PaperInfo.js
│ │ │ ├── PaperPosition.js
│ │ │ ├── PaperSearch.js
│ │ │ └── PaperSearchPosition.js
│ │ ├── images
│ │ └── DS3_logo.png
│ │ └── models
│ │ ├── PaperInfo.js
│ │ ├── PaperMetric.js
│ │ └── PaperPosition.js
└── serviceWorker.js
└── tests
└── papers
├── Papers.test.js
└── components
├── Header.test.js
├── Panel.test.js
├── Sidebar.test.js
├── header
└── HeaderModal.test.js
└── panel
└── search
├── JargonSearch.test.js
└── PaperSearch.test.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Hidden items
2 | .dockerignore
3 | .gitignore
4 | .git
5 |
6 | # Artifacts that will be built during image creation
7 | build
8 | node_modules
9 |
10 | # Development mode files
11 | public/*.js
12 | tests
13 |
14 | # Ignore any markdown document
15 | **/*.md
16 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | SERVER_API_HOST=http://0.0.0.0
2 | SERVER_API_PORT=8080
3 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 |
5 | - package-ecosystem: github-actions
6 | directory: "/"
7 | schedule:
8 | interval: weekly
9 | ignore:
10 | # Ignore auto-updates on SemVer major releases
11 | - dependency-name: "*"
12 | update-types: ["version-update:semver-major"]
13 |
14 | - package-ecosystem: npm
15 | directory: "/"
16 | schedule:
17 | interval: weekly
18 | ignore:
19 | # Ignore auto-updates on SemVer major releases
20 | - dependency-name: "*"
21 | update-types: ["version-update:semver-major"]
22 | allow:
23 | - dependency-type: development
24 | - dependency-type: production
25 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 |
4 | on:
5 | push:
6 | branches:
7 | - main
8 | paths-ignore:
9 | - "**.md"
10 | pull_request:
11 | branches:
12 | - main
13 | paths-ignore:
14 | - "**.md"
15 |
16 |
17 | concurrency:
18 | group: ${{ github.workflow }}-${{ github.ref }}
19 | cancel-in-progress: true
20 |
21 |
22 | jobs:
23 |
24 | lint:
25 | needs: []
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: "Set up GitHub Actions"
29 | uses: actions/checkout@v4
30 | - name: "Set up Node"
31 | uses: actions/setup-node@v4
32 | with:
33 | node-version: 20
34 | - name: "Install Prettier"
35 | run: npm install --global prettier@3.0.1
36 | - name: "Check format"
37 | run: make check
38 |
39 | test:
40 | needs: [lint]
41 | runs-on: ubuntu-latest
42 | steps:
43 | - name: "Set up GitHub Actions"
44 | uses: actions/checkout@v4
45 | - name: "Set up Node"
46 | uses: actions/setup-node@v4
47 | with:
48 | node-version: 20
49 | - name: "Install dependencies"
50 | run: npm install-clean
51 | - name: "Run tests"
52 | run: make test
53 |
54 | build:
55 | needs: [test]
56 | runs-on: ubuntu-latest
57 | steps:
58 | - name: "Set up GitHub Actions"
59 | uses: actions/checkout@v4
60 | - name: "Build Docker image"
61 | run: make docker-build
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS and IDE aux files
2 | **/.DS_Store
3 | .idea
4 |
5 | # Dependencies
6 | node_modules
7 |
8 | # Testing
9 | coverage
10 |
11 | # Production
12 | build
13 |
14 | # Environment files
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | # Development mode env. values file
21 | public/*.js
22 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSpacing": true,
4 | "insertPragma": false,
5 | "printWidth": 90,
6 | "tabWidth": 4,
7 | "trailingComma": "es5"
8 | }
9 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Base image
2 | FROM node:20.10-alpine
3 |
4 | # Set working directory
5 | WORKDIR /app
6 |
7 |
8 | # Copy dependency files
9 | COPY package.json /app
10 | COPY package-lock.json /app
11 |
12 | # Install dependencies
13 | RUN npm install-clean --omit=dev && \
14 | npm install --global serve && \
15 | npm cache clean --force
16 |
17 | # Copy all files
18 | COPY . /app
19 |
20 |
21 | # Build for production
22 | RUN npm run build --silent
23 |
24 |
25 | # State application exposed port
26 | EXPOSE 5000
27 |
28 | # Start app
29 | CMD ["/bin/sh", "-c", "./scripts/parse-env.sh && serve --listen tcp://0.0.0.0:5000 --single 'build'"]
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 New York University
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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | APP_VERSION = $(shell cat VERSION)
2 | IMAGE_NAME = "dialect-map-ui"
3 | SOURCE_FOLDER = "src"
4 | TESTS_FOLDER = "tests"
5 |
6 | GCP_PROJECT ?= "ds3-dialect-map"
7 | GCP_REGISTRY ?= "us.gcr.io"
8 | GCP_IMAGE_NAME = $(GCP_REGISTRY)/$(GCP_PROJECT)/$(IMAGE_NAME)
9 |
10 |
11 | .PHONY: check
12 | check:
13 | @echo "Checking code format"
14 | @npx prettier --check "$(SOURCE_FOLDER)/**/*.js"
15 | @npx prettier --check "$(TESTS_FOLDER)/**/*.js"
16 |
17 |
18 | .PHONY: build
19 | build:
20 | @echo "Building project"
21 | @npm run build --silent
22 |
23 |
24 | .PHONY: run
25 | run:
26 | @echo "Running development project"
27 | @npm run start --silent
28 |
29 |
30 | .PHONY: test
31 | test:
32 | @echo "Testing project"
33 | @npx jest --noStackTrace $(TESTS_FOLDER)
34 |
35 |
36 | .PHONY: docker-build
37 | docker-build:
38 | @echo "Building Docker image"
39 | @docker build . --tag $(IMAGE_NAME):$(APP_VERSION)
40 |
41 |
42 | .PHONY: docker-push
43 | docker-push: docker-build
44 | @echo "Pushing Docker image to GCP"
45 | @docker tag $(IMAGE_NAME):$(APP_VERSION) $(GCP_IMAGE_NAME):$(APP_VERSION)
46 | @docker push $(GCP_IMAGE_NAME):$(APP_VERSION)
47 | @docker rmi $(GCP_IMAGE_NAME):$(APP_VERSION)
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dialect map UI
2 |
3 | [![CI/CD Status][ci-status-badge]][ci-status-link]
4 | [![Coverage Status][cov-status-badge]][cov-status-link]
5 | [![MIT license][mit-license-badge]][mit-license-link]
6 | [![Code style][code-style-badge]][code-style-link]
7 |
8 | This repository contains the front-end interface of the Dialect map project.
9 | An idea proposed by professor Kyle Cranmer, and initially developed as one
10 | of the _Data Science and Software Services_ (DS3) funded projects.
11 |
12 |
13 | ## About
14 | This project provides a web interface for the [Dialect Map project][dialect-map-repo],
15 | taking the interface of the [PaperScape project][paperscape-blog] as inspiration,
16 | but modernizing and standardizing the whole stack. This web client uses:
17 |
18 | - [React][webpage-react] ⚛️
19 | - [Leaflet][webpage-leaflet] 🗺️
20 | - [Semantic][webpage-semantic] 🎨
21 |
22 |
23 | ## Environment setup
24 | All dependencies can be installed by running:
25 |
26 | ```shell
27 | npm install
28 | npm install --global serve
29 | ```
30 |
31 |
32 | ### Formatting
33 | All JavaScript files are formatted using [Prettier][webpage-prettier], and the custom properties
34 | defined in the `.prettierrc.json` file. To check for code style inconsistencies:
35 |
36 | ```shell
37 | make check
38 | ```
39 |
40 | ### Testing
41 | Project testing is performed using [Jest][webpage-jest]. In order to run the tests:
42 |
43 | ```shell
44 | make test
45 | ```
46 |
47 |
48 | ### Run project
49 | To start a development server:
50 |
51 | ```shell
52 | make run
53 | ```
54 |
55 |
56 | ## Docker
57 | The project is currently designed to be deployed in a Google Cloud Platform project,
58 | so the initial step involves using [gcloud][docs-gcloud-cli] CLI tool to log into it:
59 |
60 | ```shell
61 | gcloud login
62 | gcloud auth configure-docker
63 | ```
64 |
65 | To build a Docker image out of the project:
66 |
67 | ```shell
68 | make docker-build
69 | ```
70 |
71 | To push a Docker image to the GCP registry:
72 |
73 | ```shell
74 | export GCP_PROJECT="ds3-dialect-map"
75 | export GCP_REGISTRY="us.gcr.io"
76 | make docker-push
77 | ```
78 |
79 |
80 | ## Deployment
81 | This project uses a set of env. variables to configure the connection with the backend API:
82 |
83 | Bear in mind that React does not allow passing **run-time** env. variables to a built application
84 | ([reference][docs-react-env]). In order to do so, a dedicated shell script named `parse-env.sh` was created.
85 | This script parses the `.env` file substituting the default values by the ones defined in the environment,
86 | before writing them into a _run-time generated_ JavaScript file (loaded from the `index.html`).
87 |
88 | | ENV VARIABLE | DEFAULT | REQUIRED | DESCRIPTION |
89 | |--------------------------|--------------------|----------|-----------------------------------------------|
90 | | SERVER_API_HOST | http://0.0.0.0 | No | Backend API host to connect |
91 | | SERVER_API_PORT | 8080 | No | Backend API port to connect |
92 |
93 |
94 | ## Acknowledges
95 |
96 | We would like to thank the PaperScape authors for maintaining an up-to-date tiles server,
97 | and specially Rob J. Knegjens for being in contact with us during the development of this project.
98 |
99 |
100 | [ci-status-badge]: https://github.com/dialect-map/dialect-map-ui/actions/workflows/ci.yml/badge.svg?branch=main
101 | [ci-status-link]: https://github.com/dialect-map/dialect-map-ui/actions/workflows/ci.yml?query=branch%3Amain
102 | [code-style-badge]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg
103 | [code-style-link]: https://github.com/prettier/prettier
104 | [cov-status-badge]: https://codecov.io/gh/dialect-map/dialect-map-ui/branch/main/graph/badge.svg
105 | [cov-status-link]: https://codecov.io/gh/dialect-map/dialect-map-ui
106 | [mit-license-badge]: https://img.shields.io/badge/License-MIT-blue.svg
107 | [mit-license-link]: https://github.com/dialect-map/dialect-map-ui/blob/main/LICENSE
108 |
109 | [dialect-map-repo]: https://github.com/dialect-map/dialect-map
110 | [docs-gcloud-cli]: https://cloud.google.com/sdk/docs/install
111 | [docs-react-env]: https://create-react-app.dev/docs/adding-custom-environment-variables/
112 | [paperscape-blog]: https://paperscape.org/
113 | [webpage-jest]: https://jestjs.io/
114 | [webpage-leaflet]: https://leafletjs.com/
115 | [webpage-prettier]: https://prettier.io/docs/en/index.html
116 | [webpage-react]: https://reactjs.org/
117 | [webpage-semantic]: https://react.semantic-ui.com/
118 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.3.0
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dialect-map-ui",
3 | "version": "0.1.0",
4 | "license": "MIT",
5 | "private": true,
6 | "dependencies": {
7 | "leaflet": "^1.9.3",
8 | "react": "^17.0.1",
9 | "react-dom": "^17.0.1",
10 | "react-leaflet": "^3.2.5",
11 | "react-router-dom": "^6.2.1",
12 | "react-scripts": "^5.0.1",
13 | "semantic-ui-css": "^2.5.0",
14 | "semantic-ui-react": "^2.1.4"
15 | },
16 | "devDependencies": {
17 | "@babel/preset-env": "^7.16.0",
18 | "@babel/preset-react": "^7.16.0",
19 | "@testing-library/react": "^11.2.7",
20 | "jest": "^27.5.1",
21 | "prettier": "3.0.1",
22 | "react-test-renderer": "^17.0.2"
23 | },
24 | "scripts": {
25 | "prestart": "./scripts/parse-env.sh --destination public",
26 | "start": "react-scripts start",
27 | "build": "react-scripts build"
28 | },
29 | "babel": {
30 | "presets": [
31 | ["@babel/preset-env", {"modules": "auto"}],
32 | ["@babel/preset-react", {"runtime": "automatic"}]
33 | ],
34 | "plugins": [
35 | "@babel/plugin-transform-runtime"
36 | ]
37 | },
38 | "jest": {
39 | "testEnvironment": "jsdom",
40 | "moduleNameMapper": {
41 | "\\.(jpg|jpeg|png|gif|svg)$": "babel-jest",
42 | "\\.(css|less)$": "babel-jest"
43 | }
44 | },
45 | "browserslist": [
46 | ">0.2%",
47 | "not dead",
48 | "not op_mini all"
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dialect-map/dialect-map-ui/0791322bed1f4e57c64de4561cc30b0ca4d0440e/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
21 |
22 |
26 |
27 | Dialect map
28 |
29 |
30 |
31 |
32 | You need to enable JavaScript to run this app.
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Dialect map",
3 | "name": "Dialect map UI",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/scripts/parse-env.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ### NOTE:
4 | ###
5 | ### Script that parses every variable defined in the source file
6 | ### in order to replace the default values with environment values,
7 | ### and dump them into a JavaScript file that gets loaded by the app.
8 | ###
9 | ### Arguments:
10 | ### --destination: Destination folder within the project root (i.e. public).
11 | ### --output_file: Name of the run-time generated output JavaScript file.
12 | ### --source_file: Name of the source file to read variables names from.
13 |
14 | # Define default values
15 | destination="build"
16 | output_file="environment.js"
17 | source_file=".env"
18 |
19 | # Argument parsing
20 | while [ "$#" -gt 0 ]; do
21 | case $1 in
22 | -d|--destination) destination=${2}; shift ;;
23 | -o|--output_file) output_file=${2}; shift ;;
24 | -s|--source_file) source_file=${2}; shift ;;
25 | *) echo "Unknown parameter passed: $1"; exit 1 ;;
26 | esac
27 | shift
28 | done
29 |
30 |
31 | PROJECT_DIR="$(dirname "$0")/.."
32 |
33 | # Define file paths
34 | OUTPUT_PATH="${PROJECT_DIR}/${destination}/${output_file}"
35 | SOURCE_PATH="${PROJECT_DIR}/${source_file}"
36 |
37 | # Define AWK expressions to parse file and get env. vars
38 | AWK_PAD_EXP="\" \""
39 | AWK_KEY_EXP="\$1"
40 | AWK_VAL_EXP="(ENVIRON[\$1] ? ENVIRON[\$1] : \$2)"
41 | AWK_ALL_EXP="{ print ${AWK_PAD_EXP} ${AWK_KEY_EXP} \": '\" ${AWK_VAL_EXP} \"',\" }"
42 |
43 |
44 | # Build the run-time generated JavaScript environment file
45 | echo "window.env = {" > "${OUTPUT_PATH}"
46 | awk -F "=" "${AWK_ALL_EXP}" "${SOURCE_PATH}" >> "${OUTPUT_PATH}"
47 | echo "}" >> "${OUTPUT_PATH}"
48 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | /* NOTE:
4 | * Previously, a CORS-proxy was necessary to parse PaperScape responses
5 | * As they may not include the 'Access-Control-Allow-Origin' header.
6 | * Consider these resources if this change in the future:
7 | *
8 | * Ref: https://stackoverflow.com/questions/43262121/trying-to-use-fetch-and-pass-in-mode-no-cors
9 | * Redirect: https://cors-anywhere.herokuapp.com/
10 | */
11 |
12 | /* NOTE:
13 | *
14 | * Define browser environment fall back in order
15 | * to allow browser independent rendering (testing)
16 | */
17 | const env = window.env || {};
18 |
19 | const config = {
20 | /* Leaflet map properties */
21 | mapBoundsCoords: [
22 | [-2000, 0],
23 | [0, 2000],
24 | ],
25 | mapBoundsViscosity: 0.8,
26 | mapInitialCenter: [-1000, 1000],
27 | mapInitialZoom: 0,
28 | mapZoomMinimum: 0,
29 | mapZoomMaximum: 6,
30 | mapZoomControl: false,
31 | mapZoomDelta: 0.25,
32 | mapZoomSnap: 0.25,
33 | mapSubdomains: ["1", "2", "3", "4"],
34 |
35 | /* PaperScape map properties
36 | *
37 | * They change in a daily basis.
38 | * They need to be fetched prior any rendering
39 | */
40 | worldMinX: null,
41 | worldMaxX: null,
42 | worldMinY: null,
43 | worldMaxY: null,
44 | worldTileSize: null,
45 |
46 | viewToWorldScale: null,
47 | worldToViewScale: null,
48 |
49 | worldLabels: {
50 | 0: { nx: 1, ny: 1 },
51 | 1: { nx: 1, ny: 1 },
52 | 2: { nx: 1, ny: 1 },
53 | 3: { nx: 1, ny: 1 },
54 | 4: { nx: 2, ny: 2 },
55 | 5: { nx: 4, ny: 4 },
56 | 6: { nx: 8, ny: 8 },
57 | },
58 |
59 | /* Dialect map server */
60 | dialectMapHost: env.SERVER_API_HOST,
61 | dialectMapPort: env.SERVER_API_PORT,
62 | dialectMapURL: `${env.SERVER_API_HOST}:${env.SERVER_API_PORT}`,
63 |
64 | /* PaperScape URLs */
65 | papersDataURL: "https://paperscape.org/wombat",
66 | worldConfigURL: "https://tile1.paperscape.org/world/world_index.json",
67 | labelsJsonHost: "https://tile1.paperscape.org/world/zones",
68 |
69 | /* PaperScape tiles URLs */
70 | tilesColorHost: "https://tile{s}.paperscape.org/world/tiles/{z}/{x}/{y}.png",
71 | tilesGreyHost: "https://tile{s}.paperscape.org/world/tiles-hm/{z}/{x}/{y}.png",
72 | tilesAttrib: "PaperScape ",
73 | };
74 |
75 | export default config;
76 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 100%;
3 | margin: 0;
4 | padding: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
6 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | #root {
12 | height: 100%;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import ReactDOM from "react-dom";
4 | import * as serviceWorker from "./serviceWorker";
5 | import { BrowserRouter, Routes, Route } from "react-router-dom";
6 | import PapersMap from "./scenes/papers/Papers";
7 | import "semantic-ui-css/semantic.min.css";
8 | import "./index.css";
9 |
10 | ReactDOM.render(
11 |
12 |
13 | } />
14 | } />
15 |
16 | ,
17 | document.getElementById("root")
18 | );
19 |
20 | // If you want your app to work offline and load faster, you can change
21 | // unregister() to register() below. Note this comes with some pitfalls.
22 | // Learn more about service workers: http://bit.ly/CRA-PWA
23 | serviceWorker.register();
24 |
--------------------------------------------------------------------------------
/src/scenes/papers/Papers.css:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | .papers-layout {
4 | height: 100%;
5 | }
6 |
7 | .papers-header {
8 | min-height: 50px;
9 | min-width: 950px;
10 | padding-bottom: 0 !important; /* Necessary flag */
11 | }
12 |
13 | .papers-body {
14 | height: calc(100% - 60px);
15 | min-width: 950px;
16 | }
17 |
--------------------------------------------------------------------------------
/src/scenes/papers/Papers.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import { Grid } from "semantic-ui-react";
5 | import MapConfigCtl from "./controllers/paperscape/MapConfig";
6 | import PapersHeader from "./components/Header";
7 | import PapersPanel from "./components/Panel";
8 | import "./Papers.css";
9 |
10 | export default class PapersMap extends Component {
11 | /** Component containing the whole papers map scene */
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | configLoaded: false,
17 | };
18 |
19 | // Necessary binding to use this function in the React lifecycle
20 | this.setConfigLoaded = this.setConfigLoaded.bind(this);
21 | }
22 |
23 | componentDidMount() {
24 | MapConfigCtl.fetchConfig().then(this.setConfigLoaded);
25 | }
26 |
27 | setConfigLoaded() {
28 | this.setState({ configLoaded: true });
29 | }
30 |
31 | render() {
32 | if (this.state.configLoaded === false) {
33 | return null;
34 | }
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/Header.css:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | .papers-header-segment {
4 | border-radius: 0 !important; /* Necessary flag */
5 | padding-top: 5px !important; /* Necessary flag */
6 | padding-bottom: 5px !important; /* Necessary flag */
7 | }
8 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/Header.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import { Header, Menu, Segment } from "semantic-ui-react";
5 | import HeaderModal from "./header/HeaderModal";
6 | import "./Header.css";
7 |
8 | export default class PapersHeader extends Component {
9 | /** Component defining the global scene header */
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/Panel.css:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | .panel-body-sidebar {
4 | min-width: 100px;
5 | padding-bottom: 0 !important; /* Necessary flag */
6 | }
7 |
8 | .panel-body-main {
9 | max-width: calc(100% - 100px);
10 | padding-bottom: 0 !important; /* Necessary flag */
11 | }
12 |
13 | .panel-body-header {
14 | margin-bottom: 10px;
15 | }
16 |
17 | .panel-body-map {
18 | height: 100%;
19 | position: relative;
20 | }
21 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/Panel.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import { Grid } from "semantic-ui-react";
5 | import PapersSidebar from "./Sidebar";
6 | import JargonSearch from "./panel/search/JargonSearch";
7 | import PaperSearch from "./panel/search/PaperSearch";
8 | import MapCanvas from "./panel/map/Map";
9 | import "./Panel.css";
10 |
11 | export const PanelTabs = {
12 | JARGON_SEARCH: "jargon",
13 | PAPER_SEARCH: "paper",
14 | };
15 |
16 | export default class PapersPanel extends Component {
17 | /** Component defining the main section div (sidebar + searchbar + map) */
18 |
19 | constructor(props) {
20 | super(props);
21 | this.state = {
22 | chosenTab: PanelTabs.PAPER_SEARCH,
23 | jargonTabProperties: {
24 | extras: {},
25 | papers: [],
26 | },
27 | searchTabProperties: {
28 | papers: [],
29 | },
30 | };
31 |
32 | // Tab choosing related functions
33 | this.getChosenTab = this.getChosenTab.bind(this);
34 | this.setJargonTab = this.setJargonTab.bind(this);
35 | this.setSearchTab = this.setSearchTab.bind(this);
36 |
37 | // Papers related getters and setters
38 | this.getJargonTabExtras = this.getJargonTabExtras.bind(this);
39 | this.getJargonTabPapers = this.getJargonTabPapers.bind(this);
40 | this.setJargonTabPapers = this.setJargonTabPapers.bind(this);
41 | this.getSearchTabPapers = this.getSearchTabPapers.bind(this);
42 | this.setSearchTabPapers = this.setSearchTabPapers.bind(this);
43 | }
44 |
45 | getChosenTab() {
46 | return this.state.chosenTab;
47 | }
48 |
49 | setJargonTab() {
50 | this.setState({ chosenTab: PanelTabs.JARGON_SEARCH });
51 | }
52 |
53 | setSearchTab() {
54 | this.setState({ chosenTab: PanelTabs.PAPER_SEARCH });
55 | }
56 |
57 | getJargonTabExtras() {
58 | return this.state.jargonTabProperties.extras;
59 | }
60 |
61 | getJargonTabPapers() {
62 | return this.state.jargonTabProperties.papers;
63 | }
64 |
65 | setJargonTabPapers(papers, extras) {
66 | this.setState(prevState => ({
67 | ...prevState,
68 | jargonTabProperties: { papers: papers, extras: extras },
69 | }));
70 | }
71 |
72 | getSearchTabPapers() {
73 | return this.state.searchTabProperties.papers;
74 | }
75 |
76 | setSearchTabPapers(papers) {
77 | this.setState(prevState => ({
78 | ...prevState,
79 | searchTabProperties: { papers: papers },
80 | }));
81 | }
82 |
83 | renderTabHeader() {
84 | switch (this.state.chosenTab) {
85 | case PanelTabs.JARGON_SEARCH:
86 | return ;
87 | case PanelTabs.PAPER_SEARCH:
88 | return ;
89 | default:
90 | return ;
91 | }
92 | }
93 |
94 | render() {
95 | return (
96 |
97 |
98 |
103 |
104 |
105 |
106 |
107 | {this.renderTabHeader()}
108 |
109 |
110 |
115 |
116 |
117 |
118 | );
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/Sidebar.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import { Icon, Menu } from "semantic-ui-react";
5 | import { PanelTabs } from "./Panel";
6 |
7 | export default class PapersSidebar extends Component {
8 | /** Component defining the main section sidebar to change between tabs */
9 |
10 | isSearchActive() {
11 | return this.props.getChosenTab() === PanelTabs.PAPER_SEARCH;
12 | }
13 |
14 | isJargonActive() {
15 | return this.props.getChosenTab() === PanelTabs.JARGON_SEARCH;
16 | }
17 |
18 | render() {
19 | const { setJargonTab, setSearchTab } = this.props;
20 |
21 | return (
22 |
23 |
28 |
29 |
30 |
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/header/HeaderModal.css:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | .papers-header-info {
4 | cursor: pointer;
5 | }
6 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/header/HeaderModal.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import { Header, Image, Modal, Segment } from "semantic-ui-react";
5 | import Logo from "../../images/DS3_logo.png";
6 | import "./HeaderModal.css";
7 |
8 | export default class HeaderModal extends Component {
9 | /** Component defining the center overlaid box upon information request */
10 |
11 | render() {
12 | return (
13 |
16 | Info
17 |
18 | }
19 | >
20 | Dialect Map
21 |
22 |
23 |
24 |
25 |
26 |
27 | Dialect Map is a project initially supported by the NYU
28 | Data Science and Software Services initiative
29 |
34 | {" (DS3)"}
35 |
36 | , that compares jargon areas of influence across multiple
37 | science domains. For visualization purposes, it uses the
38 | representation of ArXiv stored papers provided by the
39 | PaperScape project.
40 |
41 |
42 | This web client has been created with the help of
43 |
48 | {" Rob J. Knegjens"}
49 |
50 | , one of the original PaperScape authors.
51 |
52 |
53 |
54 |
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/panel/map/Map.css:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | .leaflet-container {
4 | background-color: #000000;
5 | }
6 |
7 | .map-component {
8 | height: 100%;
9 | width: 100%;
10 | position: absolute;
11 | border-radius: 5px;
12 | }
13 |
14 | .panel-body-map-label {
15 | color: #FFFFFF;
16 | font-size: 14px;
17 | font-weight: bold;
18 | font-family: Arial, sans-serif;
19 | text-align: center;
20 | line-height: normal;
21 | white-space: nowrap;
22 | }
23 |
24 | .panel-body-map-info {
25 | top: 10px;
26 | right: 10px;
27 | width: 320px !important; /* Necessary flag */
28 | z-index: 450;
29 | cursor: default;
30 | position: absolute !important; /* Necessary flag */
31 | background: #FFFFFF !important; /* Necessary flag */
32 | }
33 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/panel/map/Map.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import config from "../../../../../config";
5 | import { JargonColors } from "../search/JargonSearch";
6 | import MapLayerControl from "./MapLayerControl";
7 | import MapSelectPaper from "./MapSelectPaper";
8 | import { Circle, MapContainer } from "react-leaflet";
9 | import { CRS } from "leaflet";
10 | import "leaflet/dist/leaflet.css";
11 | import "./Map.css";
12 |
13 | export default class MapCanvas extends Component {
14 | /** Component containing all the map related information / rendering */
15 |
16 | constructor(props) {
17 | super(props);
18 |
19 | // Set up at render() time
20 | this.map = null;
21 |
22 | // Necessary binding in order to access parent functions
23 | this.calcJargonColor = this.calcJargonColor.bind(this);
24 |
25 | // Necessary binding in order to pass these functions to children
26 | this.getMap = this.getMap.bind(this);
27 | this.viewToWorld = this.viewToWorld.bind(this);
28 | this.worldToView = this.worldToView.bind(this);
29 | }
30 |
31 | getMap() {
32 | return this.map;
33 | }
34 |
35 | setMap(map) {
36 | if (this.map === null) {
37 | this.map = map;
38 | }
39 | }
40 |
41 | convertRadius(radius) {
42 | return radius * config.worldToViewScale;
43 | }
44 |
45 | convertRGBColor(color) {
46 | return `rgb(${color.r}, ${color.g}, ${color.b})`;
47 | }
48 |
49 | calcJargonColor(paperId) {
50 | let jargonExtras = this.props.getJargonExtras();
51 | let paperFreqs = jargonExtras.freqByPaper[paperId];
52 |
53 | let [freqA, freqB] = Object.values(paperFreqs);
54 | let [colorA, colorB] = JargonColors.map(c => c.rgb);
55 |
56 | if (freqA === 0) {
57 | return this.convertRGBColor(colorB);
58 | }
59 | if (freqB === 0) {
60 | return this.convertRGBColor(colorA);
61 | }
62 |
63 | let colorARatio = freqB > freqA ? freqA / freqB : 1 - freqB / freqA;
64 | let colorBRatio = freqA > freqB ? freqB / freqA : 1 - freqA / freqB;
65 |
66 | return this.convertRGBColor({
67 | r: colorARatio * colorA.r + colorBRatio * colorB.r,
68 | g: colorARatio * colorA.g + colorBRatio * colorB.g,
69 | b: colorARatio * colorA.b + colorBRatio * colorB.b,
70 | });
71 | }
72 |
73 | worldToView(world_X, world_Y) {
74 | // Leaflet considers [Y, X] not [X, Y]
75 | return [
76 | -1 * (world_Y - config.worldMinY) * config.worldToViewScale,
77 | +1 * (world_X - config.worldMinX) * config.worldToViewScale,
78 | ];
79 | }
80 |
81 | viewToWorld(view_X, view_Y) {
82 | // PaperScape considers [X, Y] not [Y, X]
83 | return [
84 | +1 * view_X * config.viewToWorldScale + config.worldMinX,
85 | -1 * view_Y * config.viewToWorldScale + config.worldMinY,
86 | ];
87 | }
88 |
89 | render() {
90 | const jargonPapers = this.props.getJargonPapers();
91 | const searchPapers = this.props.getSearchPapers();
92 |
93 | return (
94 | // In react-leaflet v2, "ref" is used to obtain the created map instance
95 | // In react-leaflet v3, "whenCreated" is used to obtain the created map instance
96 | this.setMap(map)}
107 | >
108 |
113 |
114 |
119 |
120 | {jargonPapers.map((paper, index) => (
121 |
127 | ))}
128 |
129 | {searchPapers.map((paper, index) => (
130 |
136 | ))}
137 |
138 | );
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/panel/map/MapInfoBox.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import { Button, Card, Icon, List } from "semantic-ui-react";
5 |
6 | export default class MapInfoBox extends Component {
7 | /** Component defining the information box upon paper selection */
8 |
9 | buildArxivLink(arxivID) {
10 | return "https://arxiv.org/pdf/" + arxivID;
11 | }
12 |
13 | render() {
14 | const { getPaperInfo, hidePaperInfo } = this.props;
15 | const paperInfo = getPaperInfo();
16 | const arxivLink = this.buildArxivLink(paperInfo.arxivID);
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 | {paperInfo.getTitleForView()}
26 | {paperInfo.getAuthorsForView()}
27 |
28 | {paperInfo.getPublisherForView()}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {paperInfo.numRefs} references
40 |
41 |
42 |
43 | {paperInfo.numCits} citations
44 |
45 |
46 |
47 |
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/panel/map/MapLayerControl.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import config from "../../../../../config";
5 | import MapLabelsCtl from "../../../controllers/paperscape/MapLabels";
6 | import MapTileLayer from "./MapTileLayer";
7 | import { FeatureGroup, LayersControl, Marker } from "react-leaflet";
8 | import { divIcon } from "leaflet";
9 |
10 | export default class MapLayerControl extends Component {
11 | /** Component defining the box with the layer control options */
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | labels: [],
17 | };
18 |
19 | // Necessary binding in order to access parent functions
20 | this.loadLabels = this.loadLabels.bind(this);
21 | }
22 |
23 | buildLabel(label) {
24 | let labelLevels = label.split(",");
25 | labelLevels = labelLevels.filter(s => s !== "");
26 | labelLevels = labelLevels.slice(0, 3);
27 | labelLevels = labelLevels.join(" ");
28 |
29 | return labelLevels;
30 | }
31 |
32 | filterWorldLabels(label, northEast, southWest) {
33 | return (
34 | label.x >= southWest[0] &&
35 | label.x <= northEast[0] &&
36 | label.y >= northEast[1] &&
37 | label.y <= southWest[1]
38 | );
39 | }
40 |
41 | async loadLabels() {
42 | let map = this.props.getMap();
43 |
44 | let viewCenter = map.getCenter();
45 | let worldCenter = this.props.viewToWorld(viewCenter.lng, viewCenter.lat);
46 | let currentZoom = map.getZoom();
47 | let roundedZoom = Math.floor(currentZoom);
48 |
49 | let labels = await MapLabelsCtl.fetchLabels(roundedZoom, worldCenter);
50 |
51 | let viewBounds = map.getBounds();
52 | let worldNorthEast = this.props.viewToWorld(
53 | viewBounds._northEast.lng,
54 | viewBounds._northEast.lat
55 | );
56 | let worldSouthWest = this.props.viewToWorld(
57 | viewBounds._southWest.lng,
58 | viewBounds._southWest.lat
59 | );
60 |
61 | let worldLabels = labels.filter(label =>
62 | this.filterWorldLabels(label, worldNorthEast, worldSouthWest)
63 | );
64 |
65 | this.setState({
66 | labels: worldLabels,
67 | });
68 | }
69 |
70 | render() {
71 | const { worldToView } = this.props;
72 | const { labels } = this.state;
73 |
74 | return (
75 |
76 |
77 |
81 |
82 |
83 |
87 |
88 |
89 |
90 | {labels.map((label, index) => (
91 |
99 | ))}
100 |
101 |
102 |
103 | );
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/panel/map/MapSelectPaper.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import ClickHandler from "./events/MapClickEvent";
5 | import MapInfoBox from "./MapInfoBox";
6 | import { Circle } from "react-leaflet";
7 |
8 | export default class MapSelectPaper extends Component {
9 | /** Component defining the paper selection elements */
10 |
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | paperInfoVisible: false,
15 | paperInfo: null,
16 | paperPos: null,
17 | };
18 |
19 | // Necessary binding in order to pass these functions to children
20 | this.getPaperInfo = this.getPaperInfo.bind(this);
21 | this.hidePaperInfo = this.hidePaperInfo.bind(this);
22 | this.updateSelected = this.updateSelected.bind(this);
23 | }
24 |
25 | getPaperInfo() {
26 | return this.state.paperInfo;
27 | }
28 |
29 | hidePaperInfo() {
30 | this.setState({ paperInfoVisible: false });
31 | }
32 |
33 | updateSelected(paperPos, paperInfo) {
34 | this.setState({
35 | paperInfoVisible: true,
36 | paperInfo: paperInfo,
37 | paperPos: paperPos,
38 | });
39 | }
40 |
41 | render() {
42 | const { convertRadius, viewToWorld, worldToView } = this.props;
43 | const { paperPos } = this.state;
44 |
45 | return (
46 |
47 |
51 | {this.state.paperInfoVisible ? (
52 |
56 | ) : null}
57 | {this.state.paperPos !== null ? (
58 |
63 | ) : null}
64 |
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/panel/map/MapTileLayer.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import config from "../../../../../config";
4 | import { TileLayer } from "leaflet";
5 | import { createLayerComponent, updateGridLayer } from "@react-leaflet/core";
6 |
7 | class MapTileLayer {
8 | /** Class overriding the default Leaflet TileLayer functionality */
9 |
10 | static getCustomOptions() {
11 | return {
12 | attribution: config.tilesAttrib,
13 | minZoom: config.mapZoomMinimum,
14 | maxZoom: config.mapZoomMaximum,
15 | subdomains: config.mapSubdomains,
16 | tileSize: config.worldTileSize,
17 | };
18 | }
19 |
20 | static customGetTileUrl(coords) {
21 | coords.x += 1;
22 | coords.y += 1;
23 | return this.defaultGetTileUrl(coords);
24 | }
25 |
26 | static createTileLayer(props, context) {
27 | let options = MapTileLayer.getCustomOptions();
28 | let layer = new TileLayer(props.tilesURL, options);
29 |
30 | // Override default in order to correct zoom drift
31 | layer.defaultGetTileUrl = layer.getTileUrl;
32 | layer.getTileUrl = MapTileLayer.customGetTileUrl;
33 | layer.on("load", props.loadLabels);
34 |
35 | return {
36 | instance: layer,
37 | context: context,
38 | };
39 | }
40 |
41 | static updateTileLayer(instance, props, prevProps) {
42 | updateGridLayer(instance, props, prevProps);
43 |
44 | if (props.url !== prevProps.url) {
45 | instance.setUrl(props.url);
46 | }
47 | }
48 | }
49 |
50 | export default createLayerComponent(
51 | MapTileLayer.createTileLayer,
52 | MapTileLayer.updateTileLayer
53 | );
54 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/panel/map/events/MapClickEvent.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import PaperInfoCtl from "../../../../controllers/paperscape/PaperInfo";
4 | import PaperPositionCtl from "../../../../controllers/paperscape/PaperPosition";
5 | import { useMapEvent } from "react-leaflet";
6 |
7 | export default function setClickHandler({ viewToWorld, updateSelected }) {
8 | /**
9 | * Function setting the map handler for the click event.
10 | * This function must be called within a React render() context.
11 | */
12 |
13 | useMapEvent("click", e =>
14 | checkMapClick(e)
15 | .then(event => viewToWorld(event.latlng.lng, event.latlng.lat))
16 | .then(coords => clickToPaper(coords))
17 | .then(paper => updateSelected(paper.pos, paper.info))
18 | .catch(err => console.log(err.message))
19 | );
20 |
21 | return null;
22 | }
23 |
24 | async function checkMapClick(event) {
25 | // Order matters. 'Undefined' checking must go from
26 | // the most specific to the least specific property
27 | let clickedClass =
28 | event.originalEvent.target.className.baseVal ||
29 | event.originalEvent.target.className;
30 |
31 | let isDivStrings = typeof clickedClass === "string";
32 | let isDivLeaflet = clickedClass.includes("leaflet");
33 |
34 | if (isDivStrings && isDivLeaflet === false) {
35 | throw new Error("Clicked outside");
36 | } else {
37 | return event;
38 | }
39 | }
40 |
41 | async function clickToPaper(coords) {
42 | let paperPos = await PaperPositionCtl.fetchPaperPos(coords[0], coords[1]);
43 | if (paperPos === null) {
44 | throw new Error("Unknown paper coordinates");
45 | }
46 |
47 | let paperInfo = await PaperInfoCtl.fetchPaperInfo(paperPos.id);
48 | if (paperInfo === null) {
49 | throw new Error("Unknown paper ID");
50 | }
51 |
52 | return { pos: paperPos, info: paperInfo };
53 | }
54 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/panel/search/JargonSearch.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import { Button, Icon, Image, Input, Menu, Segment } from "semantic-ui-react";
5 | import JargonSearchCtl from "../../../controllers/backend/JargonSearch";
6 | import MetricSearchCtl from "../../../controllers/backend/MetricSearch";
7 | import PaperSearchCtl from "../../../controllers/paperscape/PaperSearch";
8 | import PaperSearchPositionCtl from "../../../controllers/paperscape/PaperSearchPosition";
9 | import "./Search.css";
10 |
11 | // prettier-ignore
12 | export const JargonColors = [
13 | {key: "blue", text: "blue", rgb: {r: 0, g: 0, b: 255}},
14 | {key: "red", text: "red", rgb: {r: 255, g: 0, b: 0}},
15 | ];
16 |
17 | export default class JargonSearch extends Component {
18 | /** Component to define the jargon terms comparison across papers */
19 |
20 | constructor(props) {
21 | super(props);
22 | this.state = {
23 | searchedJargonA: "",
24 | searchedJargonB: "",
25 | };
26 |
27 | // Necessary binding in order to access parent functions
28 | this.searchPapersAndExtras = this.searchPapersAndExtras.bind(this);
29 | }
30 |
31 | updateJargonA(event) {
32 | this.setState({
33 | searchedJargonA: event.target.value,
34 | });
35 | }
36 |
37 | updateJargonB(event) {
38 | this.setState({
39 | searchedJargonB: event.target.value,
40 | });
41 | }
42 |
43 | async queryJargonIds(jargons) {
44 | let promises = jargons.map(j => JargonSearchCtl.fetchJargonID(j));
45 | let results = await Promise.all(promises);
46 | return results.filter(id => id !== null);
47 | }
48 |
49 | async queryMetrics(jargonIds) {
50 | let promises = jargonIds.map(id => MetricSearchCtl.fetchLatestMetrics(id));
51 | let results = await Promise.all(promises);
52 | return results.flat();
53 | }
54 |
55 | async queryPaperIds(arxivIds) {
56 | let promises = arxivIds.map(id => PaperSearchCtl.fetchPapersIDs("saxm", id));
57 | let results = await Promise.all(promises);
58 | return results.flat();
59 | }
60 |
61 | async searchFrequenciesByPaper() {
62 | let jargons = [this.state.searchedJargonA, this.state.searchedJargonB];
63 | let jargonIds = await this.queryJargonIds(jargons);
64 | let metrics = await this.queryMetrics(jargonIds);
65 |
66 | let emptyFreqs = {};
67 | jargonIds.forEach(id => (emptyFreqs[id] = 0));
68 |
69 | let papersFreqs = {};
70 | metrics.forEach(m => (papersFreqs[m.arxivID] = { ...emptyFreqs }));
71 | metrics.forEach(m => (papersFreqs[m.arxivID][m.jargonID] = m.absFreq));
72 |
73 | return papersFreqs;
74 | }
75 |
76 | async searchPapers(arxivIds) {
77 | let paperIds = await this.queryPaperIds(arxivIds);
78 | if (paperIds.length === 0) {
79 | return [];
80 | }
81 |
82 | return await PaperSearchPositionCtl.fetchPapersPos(paperIds);
83 | }
84 |
85 | async searchPapersAndExtras() {
86 | let freqs = await this.searchFrequenciesByPaper();
87 | let axvIds = Object.keys(freqs);
88 | let papers = await this.searchPapers(axvIds);
89 | this.props.setJargonPapers(papers, { freqByPaper: freqs });
90 | }
91 |
92 | render() {
93 | return (
94 |
95 |
96 |
97 |
98 |
104 |
105 | Jargon A:
106 | this.updateJargonA(event)}
110 | />
111 |
112 |
113 |
114 |
120 |
121 | Jargon B:
122 | this.updateJargonB(event)}
126 | />
127 |
128 |
129 |
130 |
131 |
132 | Compare
133 |
134 |
135 |
136 |
137 |
138 | );
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/panel/search/PaperSearch.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { Component } from "react";
4 | import PaperSearchCtl from "../../../controllers/paperscape/PaperSearch";
5 | import PaperSearchPositionCtl from "../../../controllers/paperscape/PaperSearchPosition";
6 | import { Button, Dropdown, Icon, Image, Input, Menu, Segment } from "semantic-ui-react";
7 | import "./Search.css";
8 |
9 | // prettier-ignore
10 | export const SearchOptions = [
11 | {key: "arxiv", text: "Arxiv ID", value: "saxm"},
12 | {key: "author", text: "Author", value: "sau"},
13 | {key: "keyword", text: "Keyword", value: "skw"},
14 | {key: "title", text: "Title", value: "sti"},
15 | {key: "new-papers", text: "New papers", value: "sca"},
16 | ];
17 |
18 | export default class PaperSearch extends Component {
19 | /** Component to define the search query to the PaperScape API */
20 |
21 | constructor(props) {
22 | super(props);
23 | this.state = {
24 | paperSearch: "",
25 | paperSearchType: "",
26 | };
27 |
28 | // Necessary binding in order to access parent functions
29 | this.searchPapers = this.searchPapers.bind(this);
30 | }
31 |
32 | updateSearch(event) {
33 | this.setState({
34 | paperSearch: event.target.value,
35 | });
36 | }
37 |
38 | updateSearchType(change) {
39 | this.setState({
40 | paperSearchType: change.value,
41 | });
42 | }
43 |
44 | async searchPapers() {
45 | let ids = await PaperSearchCtl.fetchPapersIDs(
46 | this.state.paperSearchType,
47 | this.state.paperSearch
48 | );
49 | if (ids.length === 0) {
50 | return null;
51 | }
52 |
53 | let papers = await PaperSearchPositionCtl.fetchPapersPos(ids);
54 | this.props.setSearchPapers(papers);
55 | }
56 |
57 | render() {
58 | return (
59 |
60 |
61 |
62 |
63 |
64 |
65 | Search:
66 | this.updateSearch(event)}
70 | />
71 |
72 |
73 |
74 | this.updateSearchType(change)}
80 | />
81 |
82 |
83 |
84 |
85 |
86 | Search
87 |
88 |
89 |
90 |
91 |
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/scenes/papers/components/panel/search/Search.css:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | .search-container {
4 | padding-top: 5px !important; /* Necessary flag */
5 | padding-bottom: 5px !important; /* Necessary flag */
6 | }
7 |
8 | .search-menu-item {
9 | width: 28%;
10 | height: 50px !important; /* Necessary flag */
11 | }
12 |
13 | .search-menu-text {
14 | width: 150px;
15 | margin-left: 5px;
16 | margin-right: 5px;
17 | }
18 |
19 | .search-start-container {
20 | padding: 0 !important; /* Necessary flag */
21 | }
22 |
23 | .search-menu-dropdown {
24 | z-index: 1000 !important; /* Necessary flag */
25 | }
26 |
--------------------------------------------------------------------------------
/src/scenes/papers/controllers/backend/JargonSearch.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import config from "../../../../config";
4 |
5 | export default class JargonSearchCtl {
6 | /** Controller defining Jargon search queries to our own backend */
7 |
8 | static fetchJargonID(searchJargon) {
9 | // prettier-ignore
10 | let url = config.dialectMapURL
11 | + "/jargon/string/"
12 | + encodeURIComponent(searchJargon);
13 |
14 | return fetch(url, {})
15 | .then(resp => resp.json())
16 | .then(json => this._handleJargonSearchResp(json))
17 | .catch(err => console.log(err));
18 | }
19 |
20 | static _handleJargonSearchResp(json) {
21 | let jargonID = null;
22 |
23 | try {
24 | jargonID = json["jargon_id"];
25 | } catch (error) {
26 | console.log(error);
27 | }
28 |
29 | return jargonID;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/scenes/papers/controllers/backend/MetricSearch.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import config from "../../../../config";
4 | import PaperJargonMetric from "../../models/PaperMetric";
5 |
6 | export default class MetricSearchCtl {
7 | /** Controller defining metrics search queries to our own backend */
8 |
9 | static fetchLatestMetrics(jargonID) {
10 | // prettier-ignore
11 | let url = config.dialectMapURL
12 | + `/paper-metrics`
13 | + `/jargon`
14 | + `/${encodeURIComponent(jargonID)}`
15 | + `/latest`
16 |
17 | return fetch(url, {})
18 | .then(resp => resp.json())
19 | .then(json => this._handleMetricSearchResp(json))
20 | .catch(err => console.log(err));
21 | }
22 |
23 | static _handleMetricSearchResp(json) {
24 | let metrics = [];
25 |
26 | try {
27 | metrics = json.map(metric => new PaperJargonMetric(metric));
28 | } catch (error) {
29 | console.log(error);
30 | }
31 |
32 | return metrics;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/scenes/papers/controllers/paperscape/MapConfig.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import config from "../../../../config";
4 |
5 | const mapConfigRespPrefix = "world_index(";
6 | const mapConfigRespSuffix = ")";
7 |
8 | export default class MapConfigCtl {
9 | /** Controller to fetch the initial map configuration from Paperscape */
10 |
11 | static fetchConfig() {
12 | console.log("Loading PaperScape configuration...");
13 |
14 | return fetch(config.worldConfigURL, {})
15 | .then(resp => resp.text())
16 | .then(text => this._handlePaperIDResp(text));
17 | }
18 |
19 | static _calcWorldToViewScale(tilePixels, tilePixelsAtZ0) {
20 | return tilePixels / tilePixelsAtZ0;
21 | }
22 |
23 | static _calcViewToWorldScale(tilePixels, tilePixelsAtZ0) {
24 | return tilePixelsAtZ0 / tilePixels;
25 | }
26 |
27 | static _handlePaperIDResp(text) {
28 | let body = this._pruneWorldConfigResp(text);
29 | let conf = JSON.parse(body);
30 |
31 | this._updateConfig(conf);
32 | console.log("PaperScape configuration loaded");
33 | }
34 |
35 | static _pruneWorldConfigResp(body) {
36 | return body.slice(
37 | +1 * mapConfigRespPrefix.length,
38 | -1 * mapConfigRespSuffix.length
39 | );
40 | }
41 |
42 | static _updateConfig(conf) {
43 | config.worldMinX = conf["xmin"];
44 | config.worldMaxX = conf["xmax"];
45 | config.worldMinY = conf["ymin"];
46 | config.worldMaxY = conf["ymax"];
47 | config.worldTileSize = conf["pixelw"];
48 |
49 | /* IMPORTANT NOTE:
50 | *
51 | * Leaflet "Simple" CRS supposes a 1:1 ratio
52 | * Between tile pixels and world pixels at zoom 0.
53 | * As it is not the case, scaling need to be performed
54 | */
55 | config.viewToWorldScale = this._calcViewToWorldScale(
56 | conf["pixelw"],
57 | conf["tilings"][0]["tw"]
58 | );
59 | config.worldToViewScale = this._calcWorldToViewScale(
60 | conf["pixelw"],
61 | conf["tilings"][0]["tw"]
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/scenes/papers/controllers/paperscape/MapLabels.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import config from "../../../../config";
4 |
5 | const mapLabelsRespPrefix = "lz_Z_X_Y(";
6 | const mapLabelsRespSuffix = ")";
7 |
8 | export default class MapLabelsCtl {
9 | /** Controller defining the map labels queries to the Paperscape API */
10 |
11 | static fetchLabels(zoomLevel, centerPos) {
12 | let labelSpec = config.worldLabels[zoomLevel];
13 | let labelsXTile = this._getLabelTile(
14 | centerPos[0],
15 | config.worldMinX,
16 | config.worldMaxX,
17 | labelSpec.nx
18 | );
19 | let labelsYTile = this._getLabelTile(
20 | centerPos[1],
21 | config.worldMinY,
22 | config.worldMaxY,
23 | labelSpec.ny
24 | );
25 |
26 | // prettier-ignore
27 | let url = config.labelsJsonHost
28 | + "/" + zoomLevel
29 | + "/" + labelsXTile
30 | + "/" + labelsYTile
31 | + ".json";
32 |
33 | return fetch(url, {})
34 | .then(resp => resp.text())
35 | .then(text => this._handleLabelsResp(text))
36 | .catch(err => console.log(err));
37 | }
38 |
39 | static _getLabelTile(coord, coordMin, coordMax, tilesNum) {
40 | let chunkSize = (coordMax - coordMin) / tilesNum;
41 | let highBound = coordMin + chunkSize;
42 | let tileIndex = 1;
43 |
44 | while (coord >= highBound) {
45 | highBound += chunkSize;
46 | tileIndex += 1;
47 | }
48 |
49 | return tileIndex;
50 | }
51 |
52 | static _handleLabelsResp(text) {
53 | let body = this._pruneLabelsResp(text);
54 | let json = JSON.parse(body);
55 | return json["lbls"];
56 | }
57 |
58 | static _pruneLabelsResp(body) {
59 | return body.slice(
60 | +1 * mapLabelsRespPrefix.length,
61 | -1 * mapLabelsRespSuffix.length
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/scenes/papers/controllers/paperscape/PaperInfo.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import config from "../../../../config";
4 | import PaperInfo from "../../models/PaperInfo";
5 |
6 | const paperInfoRespPrefix = "(";
7 | const paperInfoRespSuffix = ")\n";
8 |
9 | export default class PaperInfoCtl {
10 | /** Controller defining the paper info queries to the Paperscape API */
11 |
12 | static fetchPaperInfo(paperID) {
13 | // prettier-ignore
14 | let url = config.papersDataURL
15 | + "?callback="
16 | + "&flags[]=1"
17 | + "&gdata[]=" + paperID;
18 |
19 | return fetch(url, {})
20 | .then(resp => resp.text())
21 | .then(text => this._handlePaperInfoResp(text));
22 | }
23 |
24 | static _handlePaperInfoResp(text) {
25 | let paperInfo = null;
26 |
27 | try {
28 | let body = this._prunePaperInfoResp(text);
29 | let json = JSON.parse(body);
30 | let data = json["r"]["papr"][0];
31 | paperInfo = new PaperInfo(data);
32 | } catch (error) {
33 | console.log(error);
34 | }
35 |
36 | return paperInfo;
37 | }
38 |
39 | static _prunePaperInfoResp(body) {
40 | return body.slice(
41 | +1 * paperInfoRespPrefix.length,
42 | -1 * paperInfoRespSuffix.length
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/scenes/papers/controllers/paperscape/PaperPosition.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import config from "../../../../config";
4 | import PaperPosition from "../../models/PaperPosition";
5 |
6 | const paperPositionRespPrefix = "(";
7 | const paperPositionRespSuffix = ")\n";
8 |
9 | export default class PaperPositionCtl {
10 | /** Controller defining the paper coordinates queries to the Paperscape API */
11 |
12 | static fetchPaperPos(X_pos, Y_pos) {
13 | // prettier-ignore
14 | let url = config.papersDataURL
15 | + "?callback="
16 | + "&tbl="
17 | + "&ml2p[]=" + X_pos
18 | + "&ml2p[]=" + Y_pos;
19 |
20 | return fetch(url, {})
21 | .then(resp => resp.text())
22 | .then(text => this._handlePaperPosResp(text));
23 | }
24 |
25 | static _handlePaperPosResp(text) {
26 | let paperPos = null;
27 |
28 | try {
29 | let body = this._prunePaperPosResp(text);
30 | let json = JSON.parse(body);
31 | let data = json["r"];
32 | paperPos = new PaperPosition(data);
33 | } catch (error) {
34 | console.log(error);
35 | }
36 |
37 | return paperPos;
38 | }
39 |
40 | static _prunePaperPosResp(body) {
41 | return body.slice(
42 | +1 * paperPositionRespPrefix.length,
43 | -1 * paperPositionRespSuffix.length
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/scenes/papers/controllers/paperscape/PaperSearch.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import config from "../../../../config";
4 |
5 | const paperSearchRespPrefix = "(";
6 | const paperSearchRespSuffix = ")\n";
7 |
8 | export default class PaperSearchCtl {
9 | /** Controller defining the paper search queries to the Paperscape API */
10 |
11 | static fetchPapersIDs(searchKey, searchValue) {
12 | // prettier-ignore
13 | let url = config.papersDataURL
14 | + this._buildRequestParams(searchKey, searchValue);
15 |
16 | return fetch(url, {})
17 | .then(resp => resp.text())
18 | .then(text => this._handlePaperSearchResp(text));
19 | }
20 |
21 | static _buildRequestParams(searchKey, searchValue) {
22 | let params = "?callback=";
23 |
24 | switch (searchKey) {
25 | case "saxm":
26 | params += "&saxm=" + searchValue;
27 | break;
28 | case "sau":
29 | params += "&sau=" + searchValue;
30 | break;
31 | case "skw":
32 | params += "&skw=" + searchValue;
33 | break;
34 | case "sti":
35 | params += "&sti=" + searchValue;
36 | break;
37 | case "sca":
38 | params += "&sca=" + searchValue + "&fd=1&td=0";
39 | break;
40 | default:
41 | params += "&skw=" + searchValue;
42 | break;
43 | }
44 |
45 | return params;
46 | }
47 |
48 | static _handlePaperSearchResp(text) {
49 | let paperIDs = [];
50 |
51 | try {
52 | let body = this._prunePaperSearchResp(text);
53 | let json = JSON.parse(body);
54 | let data = json["r"];
55 | paperIDs = data.map(paper => paper.id);
56 | } catch (error) {
57 | console.log(error);
58 | }
59 |
60 | return paperIDs;
61 | }
62 |
63 | static _prunePaperSearchResp(body) {
64 | return body.slice(
65 | +1 * paperSearchRespPrefix.length,
66 | -1 * paperSearchRespSuffix.length
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/scenes/papers/controllers/paperscape/PaperSearchPosition.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import config from "../../../../config";
4 | import PaperPosition from "../../models/PaperPosition";
5 |
6 | export default class PaperSearchPositionCtl {
7 | /** Controller defining the paper position search queries to the Paperscape API */
8 |
9 | static fetchPapersPos(paperIDs) {
10 | let url = config.papersDataURL;
11 | let params = {
12 | method: "POST",
13 | headers: { "Content-Type": "application/x-www-form-urlencoded" },
14 | body: this._buildRequestBody(paperIDs),
15 | };
16 |
17 | return fetch(url, params)
18 | .then(resp => resp.json())
19 | .then(json => this._handlePaperPosResp(json));
20 | }
21 |
22 | static _buildRequestBody(paperIDs) {
23 | // prettier-ignore
24 | paperIDs = paperIDs.map((id) => "&mp2l[]=" + id);
25 | paperIDs = paperIDs.join("");
26 | return "tbl=" + paperIDs;
27 | }
28 |
29 | static _handlePaperPosResp(json) {
30 | let paperPos = [];
31 |
32 | try {
33 | let data = json["r"];
34 | paperPos = data.map(paper => new PaperPosition(paper));
35 | } catch (error) {
36 | console.log(error);
37 | }
38 |
39 | return paperPos;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/scenes/papers/images/DS3_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dialect-map/dialect-map-ui/0791322bed1f4e57c64de4561cc30b0ca4d0440e/src/scenes/papers/images/DS3_logo.png
--------------------------------------------------------------------------------
/src/scenes/papers/models/PaperInfo.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | export default class PaperInfo {
4 | /** Model containing the paper information structure */
5 |
6 | maxCharsPerField = 40;
7 |
8 | // prettier-ignore
9 | constructor(data) {
10 | // Some of these fields may not be in the API response
11 | this.title = data["titl"] || "";
12 | this.auth = data["auth"] || "";
13 | this.publ = data["publ"] || "";
14 | this.arxivID = data["arxv"] || "";
15 | this.numRefs = data["nr"];
16 | this.numCits = data["nc"];
17 | }
18 |
19 | getTitleForView() {
20 | let shortTitle = this.title.substring(0, this.maxCharsPerField);
21 | if (shortTitle.length < this.title.length) {
22 | shortTitle += "...";
23 | }
24 | return shortTitle;
25 | }
26 |
27 | getAuthorsForView() {
28 | let shortAuthors = this.auth.substring(0, this.maxCharsPerField);
29 | if (shortAuthors.length < this.auth.length) {
30 | shortAuthors += "...";
31 | }
32 | return shortAuthors;
33 | }
34 |
35 | getPublisherForView() {
36 | let shortPublisher = this.publ.substring(0, this.maxCharsPerField);
37 | if (shortPublisher.length < this.publ.length) {
38 | shortPublisher += "...";
39 | }
40 | return shortPublisher;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/scenes/papers/models/PaperMetric.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | export default class PaperJargonMetric {
4 | /** Model containing the basic jargon metric structure */
5 |
6 | // prettier-ignore
7 | constructor(data) {
8 | this.id = data["metric_id"];
9 | this.jargonID = data["jargon_id"];
10 | this.arxivID = data["arxiv_id"];
11 | this.arxivRev = data["arxiv_rev"];
12 | this.absFreq = data["abs_freq"];
13 | this.relFreq = data["rel_freq"];
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/scenes/papers/models/PaperPosition.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | export default class PaperPosition {
4 | /** Model containing the basic paper position structure */
5 |
6 | // prettier-ignore
7 | constructor(data) {
8 | this.id = data["id"];
9 | this.x = data["x"];
10 | this.y = data["y"];
11 | this.r = data["r"];
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read http://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === "localhost" ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === "[::1]" ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener("load", () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | "This web app is being served cache-first by a service " +
46 | "worker. To learn more, visit http://bit.ly/CRA-PWA"
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === "installed") {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | "New content is available and will be used when all " +
74 | "tabs for this page are closed. See http://bit.ly/CRA-PWA."
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log("Content is cached for offline use.");
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error("Error during service worker registration:", error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get("content-type");
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf("javascript") === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log("No internet connection found. App is running in offline mode.");
124 | });
125 | }
126 |
127 | export function unregister() {
128 | if ("serviceWorker" in navigator) {
129 | navigator.serviceWorker.ready.then(registration => {
130 | registration.unregister();
131 | });
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/tests/papers/Papers.test.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { render } from "@testing-library/react";
4 | import MapConfigCtl from "../../src/scenes/papers/controllers/paperscape/MapConfig";
5 | import PapersMap from "../../src/scenes/papers/Papers";
6 |
7 | describe("Papers scene", () => {
8 | /** Set of tests for the Papers scene component */
9 |
10 | beforeEach(() => {
11 | MapConfigCtl.fetchConfig = jest.fn().mockResolvedValue({});
12 | });
13 |
14 | test("Mounts zero components by default", () => {
15 | const { container } = render( );
16 | const numberChildren = container.children.length;
17 | expect(numberChildren).toBe(0);
18 | });
19 |
20 | test("Mounts some components upon config loading", async () => {
21 | const { container } = render( );
22 | await MapConfigCtl.fetchConfig;
23 | const numberChildren = container.children.length;
24 | expect(numberChildren).toBeGreaterThan(0);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/tests/papers/components/Header.test.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { render } from "@testing-library/react";
4 | import PapersHeader from "../../../src/scenes/papers/components/Header";
5 |
6 | describe("Header component", () => {
7 | /** Set of tests for the Header component */
8 |
9 | test("Shows header title", () => {
10 | const { getByText } = render( );
11 | const headerTitle = getByText("Dialect Map");
12 | expect(headerTitle).toBeDefined();
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/tests/papers/components/Panel.test.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import renderer from "react-test-renderer";
4 | import PapersPanel, { PanelTabs } from "../../../src/scenes/papers/components/Panel";
5 |
6 | describe("Panel component", () => {
7 | /** Set of tests for the Panel component */
8 |
9 | test("Sets jargon search tab", () => {
10 | const wrapper = renderer.create( );
11 | const instance = wrapper.getInstance();
12 |
13 | instance.state.chosenTab = null;
14 | instance.setJargonTab();
15 |
16 | const currentTab = instance.state.chosenTab;
17 | expect(currentTab).toBe(PanelTabs.JARGON_SEARCH);
18 | });
19 |
20 | test("Sets paper search tab", () => {
21 | const wrapper = renderer.create( );
22 | const instance = wrapper.getInstance();
23 |
24 | instance.state.chosenTab = null;
25 | instance.setSearchTab();
26 |
27 | const currentTab = instance.state.chosenTab;
28 | expect(currentTab).toBe(PanelTabs.PAPER_SEARCH);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/tests/papers/components/Sidebar.test.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { render } from "@testing-library/react";
4 | import PapersSidebar from "../../../src/scenes/papers/components/Sidebar";
5 |
6 | describe("Sidebar component", () => {
7 | /** Set of tests for the Sidebar component */
8 |
9 | test("Shows menu icons", () => {
10 | const { container } = render( {}} />);
11 | const sidebarIcons = container.getElementsByClassName("circular icon");
12 | expect(sidebarIcons).toHaveLength(2);
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/tests/papers/components/header/HeaderModal.test.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { render, fireEvent } from "@testing-library/react";
4 | import HeaderModal from "../../../../src/scenes/papers/components/header/HeaderModal";
5 |
6 | describe("HeaderModal component", () => {
7 | /** Set of tests for the HeaderModal component */
8 |
9 | test("Shows modal button", () => {
10 | const { getByText } = render( );
11 | const modalButton = getByText("Info");
12 | expect(modalButton).toBeDefined();
13 | });
14 |
15 | test("Shows modal content upon click", async () => {
16 | const { getByText } = render( );
17 | const button = getByText("Info");
18 | fireEvent.click(button);
19 |
20 | const modelContentTitle = getByText("Project description");
21 | expect(modelContentTitle).toBeDefined();
22 | });
23 |
24 | test("Hides modal content prior click", async () => {
25 | const { queryByText } = render( );
26 | const modelContentTitle = queryByText("Project description");
27 | expect(modelContentTitle).toBeNull();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/tests/papers/components/panel/search/JargonSearch.test.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { render } from "@testing-library/react";
4 | import renderer from "react-test-renderer";
5 | import JargonSearch from "../../../../../src/scenes/papers/components/panel/search/JargonSearch";
6 | import JargonSearchCtl from "../../../../../src/scenes/papers/controllers/backend/JargonSearch";
7 | import MetricSearchCtl from "../../../../../src/scenes/papers/controllers/backend/MetricSearch";
8 | import PaperSearchCtl from "../../../../../src/scenes/papers/controllers/paperscape/PaperSearch";
9 |
10 | describe("JargonSearch component", () => {
11 | /** Set of tests for the JargonSearch component */
12 |
13 | test("Updates search jargon A", () => {
14 | const wrapper = renderer.create( );
15 | const instance = wrapper.getInstance();
16 | const testValue = "example";
17 | const testEvent = { target: { value: testValue } };
18 |
19 | instance.state.searchedJargonA = null;
20 | instance.updateJargonA(testEvent);
21 |
22 | const searchJargon = instance.state.searchedJargonA;
23 | expect(searchJargon).toBe(testValue);
24 | });
25 |
26 | test("Updates search jargon B", () => {
27 | const wrapper = renderer.create( );
28 | const instance = wrapper.getInstance();
29 | const testValue = "example";
30 | const testEvent = { target: { value: testValue } };
31 |
32 | instance.state.searchedJargonB = null;
33 | instance.updateJargonB(testEvent);
34 |
35 | const searchJargon = instance.state.searchedJargonB;
36 | expect(searchJargon).toBe(testValue);
37 | });
38 |
39 | test("Gets Jargon IDs", async () => {
40 | JargonSearchCtl.fetchJargonID = jest.fn().mockResolvedValue(null);
41 |
42 | const wrapper = renderer.create( );
43 | const instance = wrapper.getInstance();
44 | const results = await instance.queryJargonIds(["ID_1", "ID_2"]);
45 | expect(results).toHaveLength(0);
46 | });
47 |
48 | test("Gets Jargon metrics", async () => {
49 | MetricSearchCtl.fetchLatestMetrics = jest.fn().mockResolvedValue(["a"]);
50 |
51 | const wrapper = renderer.create( );
52 | const instance = wrapper.getInstance();
53 | const results = await instance.queryMetrics(["ID_1", "ID_2"]);
54 | expect(results).toStrictEqual(["a", "a"]);
55 | });
56 |
57 | test("Gets Paper IDs", async () => {
58 | PaperSearchCtl.fetchPapersIDs = jest.fn().mockResolvedValue(["b"]);
59 |
60 | const wrapper = renderer.create( );
61 | const instance = wrapper.getInstance();
62 | const results = await instance.queryPaperIds(["ID_1", "ID_2"]);
63 | expect(results).toStrictEqual(["b", "b"]);
64 | });
65 |
66 | test("Shows search HTML elements", () => {
67 | const { container } = render( );
68 |
69 | const searchIcons = container.getElementsByClassName("circular icon");
70 | const searchInput = container.getElementsByClassName("input");
71 |
72 | expect(searchIcons).toHaveLength(2);
73 | expect(searchInput).toHaveLength(2);
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/tests/papers/components/panel/search/PaperSearch.test.js:
--------------------------------------------------------------------------------
1 | /* encoding: utf-8 */
2 |
3 | import { render } from "@testing-library/react";
4 | import renderer from "react-test-renderer";
5 | import PaperSearch from "../../../../../src/scenes/papers/components/panel/search/PaperSearch";
6 | import PaperSearchCtl from "../../../../../src/scenes/papers/controllers/paperscape/PaperSearch";
7 |
8 | describe("PaperSearch component", () => {
9 | /** Set of tests for the PaperSearch component */
10 |
11 | test("Updates search input", () => {
12 | const wrapper = renderer.create( );
13 | const instance = wrapper.getInstance();
14 | const testValue = "example";
15 | const testEvent = { target: { value: testValue } };
16 |
17 | instance.state.paperSearch = null;
18 | instance.updateSearch(testEvent);
19 |
20 | const searchValue = instance.state.paperSearch;
21 | expect(searchValue).toBe(testValue);
22 | });
23 |
24 | test("Updates search type", () => {
25 | const wrapper = renderer.create( );
26 | const instance = wrapper.getInstance();
27 | const testValue = "example";
28 | const testEvent = { value: testValue };
29 |
30 | instance.state.paperSearchType = null;
31 | instance.updateSearchType(testEvent);
32 |
33 | const searchType = instance.state.paperSearchType;
34 | expect(searchType).toBe(testValue);
35 | });
36 |
37 | test("Handles empty results", async () => {
38 | PaperSearchCtl.fetchPapersIDs = jest.fn().mockResolvedValue([]);
39 |
40 | const wrapper = renderer.create( );
41 | const instance = wrapper.getInstance();
42 | const results = await instance.searchPapers();
43 | expect(results).toBeNull();
44 | });
45 |
46 | test("Shows search HTML elements", () => {
47 | const { container, getByText } = render( );
48 |
49 | const searchLabel = getByText("Search:");
50 | const searchDrop = container.getElementsByClassName("selection dropdown");
51 | const searchIcons = container.getElementsByClassName("circular icon");
52 | const searchInput = container.getElementsByClassName("input");
53 |
54 | expect(searchLabel).toBeDefined();
55 | expect(searchDrop).toHaveLength(1);
56 | expect(searchIcons).toHaveLength(1);
57 | expect(searchInput).toHaveLength(1);
58 | });
59 | });
60 |
--------------------------------------------------------------------------------