Compare commits

...

46 Commits
map ... main

Author SHA1 Message Date
gman 409f20f8cc center popup on point
2 years ago
gman addf83357b virtual scroller, sticky header
2 years ago
gman 786d7c99b5 remove view options
2 years ago
gman 6ea1cb398d simplify filter update
2 years ago
gman 9023481a1b bump vuestic
2 years ago
gman 9e6ab51387 fix colors
2 years ago
gman f78ecde04b fix: limit rendered rows in table
2 years ago
w2 1f099689a3 add: basic upload error reporting
2 years ago
gtitov 15bf8342a8 msc: change martin url
3 years ago
gtitov 9ebaa873e7 msc: fields popup on click not dblclick
3 years ago
gtitov 0dc41fd58e msc: change martin url
3 years ago
rrr-marble 49d7560df7 fix: failure to display item map dot
3 years ago
rrr-marble dc3abab13b fix: text search crush site
3 years ago
g 3707dda2df add: white labels in layers list
3 years ago
rrr-marble 8d6ed36b00 add: form dimensions filter
3 years ago
rrr-marble 7c8a644df0 msc: remove redundunt log
3 years ago
rrr-marble 28a8d6c07a add: copy cart logic
3 years ago
rrr-marble 8a61661004 fix: main page popover is hidden
3 years ago
rrr-marble 8b53e5d6ff fix: project filterdoes nothing
3 years ago
rrr-marble 3ca855ff06 add: resolution filter
3 years ago
rrr-marble 14ea755a51 msc: type resolution as float
3 years ago
rrr-marble 81ebc2cb5f add: datalist filter
3 years ago
rrr-marble 348a733cbe add: description filter
3 years ago
rrr-marble f5ade24487 add: project filter
3 years ago
rrr-marble 0c8e5b9b61 fix: default map view
3 years ago
rrr-marble 195486921f msc: adjust shown data table columns
3 years ago
rrr-marble ada5d769db msc: adjust shown item details
3 years ago
rrr-marble 8f912cb8f1 add: copy cart proto
3 years ago
rrr-marble 3f527cb8bb add: base map switcher logic
3 years ago
rrr-marble 81b2968a6a add: base map switcher proto
3 years ago
rrr-marble 24024790b5 add: fields layer labeles popup
3 years ago
rrr-marble 60fa0cc4e0 add: fields layer w/ controls
3 years ago
rrr-marble 02e4fca694 msc: adjust open ports
3 years ago
rrr-marble 395f2bd09e add: env vars for shared jwt secret
3 years ago
rrr-marble 7eacf33cde fix: frontend build warnings
3 years ago
rrr-marble cc1124e475 add: frontend jwt cookies logic
3 years ago
rrr-marble e34355cc7b add: caddy jwt logic
3 years ago
rrr-marble b89fcc40fd add: caddy jwt module
3 years ago
rrr-marble 1a22986f5a add: frontend cart logic
3 years ago
rrr-marble b022524696 add: save items in localStorage cart
3 years ago
rrr-marble 1671531857 add: empty cart
3 years ago
rrr-marble c7dd8b7935 add: sidebar anchor for similar items
3 years ago
rrr-marble 66aaa11b25 add: similar items list in details
3 years ago
rrr-marble c609db7f8d msc: inner scroll instead of paging
3 years ago
rrr-marble 3376e76578 dep: bump vuestic-ui
3 years ago
rrr-marble 1af311c562 msc: rename columns
3 years ago

@ -10,6 +10,8 @@
# Production
- add domain to *Caddyfile*
- generate new database password and put it into appropriate places in *docker/docker-compose.yml*
- generate new `SECRET_KEY` with `openssl rand -base64 32` and put it into *docker/docker-compose.yml* geodata: and frontend: environment variables
# Run
- `docker-compose --file docker/docker-compose.yml up --build -d`

@ -1,3 +1,5 @@
import os
from base64 import b64decode
from datetime import datetime, timedelta
from re import IGNORECASE, sub as substitute
@ -16,7 +18,7 @@ from .database import SessionLocal, engine
# Security
# take it from env
SECRET_KEY = b64decode("iYg7wB+sPihtjz50iJTsD0XmOeUwKy2TJtfNLcqFRM8=").hex()
SECRET_KEY = b64decode(os.environ["SECRET_KEY"])
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 3600
@ -199,7 +201,15 @@ def create_items(
for row in sheet["data"]
]
# dump all the data into database
try:
accepted, processed = crud.insert_items(db=db, items=spreadsheet_item_list)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="{}".format(
e
),
)
# transform spreadsheet coordinates data into spacial
crud.add_spacial_data(db=db)

@ -5,6 +5,7 @@ from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
@ -45,6 +46,7 @@ def run_migrations_offline() -> None:
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
)
with context.begin_transaction():
@ -65,7 +67,11 @@ def run_migrations_online() -> None:
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()

@ -0,0 +1,38 @@
"""Cast resolution as float
Revision ID: 15b3c7a8e804
Revises: 4968a333a100
Create Date: 2022-10-31 23:55:04.806055
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "15b3c7a8e804"
down_revision = "4968a333a100"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands MANUALLY composed for Alembic ###
op.execute(
sa.text(
"ALTER TABLE geodata ALTER COLUMN resolution TYPE FLOAT USING resolution::double precision"
)
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic ###
op.alter_column(
"geodata",
"resolution",
existing_type=sa.Float(),
type_=sa.VARCHAR(),
existing_nullable=True,
)
# ### end Alembic commands ###

@ -89,28 +89,25 @@ def upgrade() -> None:
[
{"database": "fadr", "spreadsheet": "fadr"},
{"database": "internal_id", "spreadsheet": "Уникальный номер образца"},
{"database": "x_coord", "spreadsheet": "кордината X"},
{"database": "y_coord", "spreadsheet": "координата Y"},
{"database": "x_coord", "spreadsheet": "Широта"},
{"database": "y_coord", "spreadsheet": "Долгота"},
{
"database": "gis_category",
"spreadsheet": """Категория в ГИС
Geology - керн, каменный материал
Soil - почва, грунты, морские осадки
Material - искуственные материалы, минералы""",
"database": "category",
"spreadsheet": "Категория",
},
{
"database": "category",
"spreadsheet": "Категория (Археология, Биология, Минералы)",
"database": "gis_category",
"spreadsheet": """Категория в ГИС""",
},
{"database": "basin", "spreadsheet": "Бассейн"},
{"database": "deposit", "spreadsheet": "Месторождение"},
{"database": "well", "spreadsheet": "№ скважины"},
{"database": "depth", "spreadsheet": "Глубина, интервал, привязка"},
{"database": "stratum", "spreadsheet": "Свита\пласт"},
{"database": "owner", "spreadsheet": "Хозяин (ФИО)"},
{"database": "depth", "spreadsheet": "Глубины"},
{"database": "stratum", "spreadsheet": "Свита / пласт"},
{"database": "owner", "spreadsheet": "Владелец"},
{"database": "org", "spreadsheet": "Организация"},
{"database": "ownercontacts", "spreadsheet": "Контакты хозяина"},
{"database": "samplelist", "spreadsheet": "перечень объектов/образцов"},
{"database": "ownercontacts", "spreadsheet": "Контакты"},
{"database": "samplelist", "spreadsheet": "Перечень объектов / образцов"},
{"database": "description", "spreadsheet": "Описание объекта"},
{"database": "form_dimentions", "spreadsheet": "Форма, размер"},
{"database": "datalist", "spreadsheet": "Перечень данных"},
@ -118,7 +115,7 @@ Soil - почва, грунты, морские осадки
{"database": "date", "spreadsheet": "Дата съёмки"},
{"database": "additional_info", "spreadsheet": "Дополнительная информация"},
{"database": "scanner", "spreadsheet": "Томограф"},
{"database": "comment", "spreadsheet": "комментарий"},
{"database": "comment", "spreadsheet": "Комментарий"},
],
)

@ -9,8 +9,9 @@ from sqlalchemy.dialects.postgresql import TSVECTOR
# if we are run as a module or as a script
# https://stackoverflow.com/questions/14132789/relative-imports-for-the-billionth-time/14132912
import sys
parent_module = sys.modules['.'.join(__name__.split('.')[:-1]) or '__main__']
if __name__ == '__main__' or parent_module.__name__ == '__main__':
parent_module = sys.modules[".".join(__name__.split(".")[:-1]) or "__main__"]
if __name__ == "__main__" or parent_module.__name__ == "__main__":
from database import Base
else:
from .database import Base
@ -45,7 +46,7 @@ class ItemBase(Base):
description = Column(String)
form_dimentions = Column(String)
datalist = Column(String)
resolution = Column(String)
resolution = Column(Float)
date = Column(String)
additional_info = Column(String)
scanner = Column(String)

@ -69,7 +69,7 @@ class ItemBase(BaseModel):
description: Optional[str] = None
form_dimentions: Optional[str] = None
datalist: Optional[str] = None
resolution: Optional[str] = None
resolution: Optional[float] = None
date: Optional[Union[datetime, str]] = None
additional_info: Optional[str] = None
scanner: Optional[str] = None

@ -1,3 +1,7 @@
{
order jwtauth before basicauth
}
:80 {
encode zstd gzip
@ -8,16 +12,24 @@
redir /openapi.json /api/v1/openapi.json permanent
handle_path /martin/* {
rewrite * {path}
reverse_proxy martin:3000
}
@is_admin {
vars {http.auth.user.id} "demo"
}
handle_path /pgweb/* {
jwtauth {
sign_key {$SECRET_KEY}
from_cookies user_session
}
rewrite * {path}
reverse_proxy pgweb:8081
reverse_proxy @is_admin pgweb:8081
redir /login/ 401
}
handle_path /static/previews/* {
@ -32,5 +44,4 @@
try_files {path}.html {path} /index.html
file_server
}
}

@ -1,12 +1,18 @@
# build stage
FROM caddy:2-builder-alpine as xcaddy-stage
RUN xcaddy build --with github.com/ggicci/caddy-jwt
FROM node:12.22.12-bullseye as build-stage
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npx browserslist@latest --update-db
RUN npm run build
# production stage
FROM caddy:2-alpine as production-stage
COPY --from=xcaddy-stage /usr/bin/caddy /usr/bin/caddy
COPY --from=build-stage /app/dist /usr/share/caddy
EXPOSE 80

@ -8,6 +8,8 @@ services:
dockerfile: ../../docker/Dockerfile.backend
volumes:
- tmp_vol:/tmp
environment:
- SECRET_KEY="iYg7wB+sPihtjz50iJTsD0XmOeUwKy2TJtfNLcqFRM8="
postgres:
image: "postgis/postgis:13-3.2"
@ -32,17 +34,18 @@ services:
dockerfile: ../docker/Dockerfile.frontend
ports:
- "80:80"
- "443:443"
volumes:
- caddy_data:/data
- caddy_config:/config
- ../caddy:/etc/caddy
- ../previews:/srv
environment:
- SECRET_KEY="iYg7wB+sPihtjz50iJTsD0XmOeUwKy2TJtfNLcqFRM8="
pgweb:
restart: always
image: sosedoff/pgweb:0.11.12
ports:
- "8081:8081"
environment:
DATABASE_URL: "postgres://geodata:QAKvBKvLe4bS9U@postgres/geodata?sslmode=disable" # TODO: change to real password

@ -11,9 +11,10 @@
"maplibre-gl": "^2.1.9",
"vue": "^3.2.13",
"vue-router": "^4.1.5",
"vuestic-ui": "^1.4.7"
"vuestic-ui": "^1.4.13"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@vue/cli-service": "~5.0.0",
"nodemon": "^2.0.19",
"vue-cli-plugin-vuestic-ui": "~1.0.8"
@ -38,7 +39,6 @@
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
"dev": true,
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.1.0",
"@jridgewell/trace-mapping": "^0.3.9"
@ -73,7 +73,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz",
"integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==",
"dev": true,
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
@ -104,7 +103,6 @@
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true,
"peer": true,
"bin": {
"json5": "lib/cli.js"
},
@ -117,7 +115,6 @@
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.10.tgz",
"integrity": "sha512-0+sW7e3HjQbiHbj1NeU/vN8ornohYlacAfZIaXhdoGweQqgcNy69COVciYYqEXJ/v+9OBA7Frxm4CVAuNqKeNA==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/types": "^7.18.10",
"@jridgewell/gen-mapping": "^0.3.2",
@ -132,7 +129,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"peer": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@ -165,7 +161,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
"dev": true,
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@ -175,7 +170,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz",
"integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/template": "^7.18.6",
"@babel/types": "^7.18.9"
@ -189,7 +183,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/types": "^7.18.6"
},
@ -202,7 +195,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/types": "^7.18.6"
},
@ -215,7 +207,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz",
"integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-module-imports": "^7.18.6",
@ -235,7 +226,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz",
"integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/types": "^7.18.6"
},
@ -248,7 +238,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/types": "^7.18.6"
},
@ -261,7 +250,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz",
"integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==",
"dev": true,
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@ -289,7 +277,6 @@
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz",
"integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/template": "^7.18.6",
"@babel/traverse": "^7.18.9",
@ -391,7 +378,6 @@
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
"integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.18.10",
@ -406,7 +392,6 @@
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.10.tgz",
"integrity": "sha512-J7ycxg0/K9XCtLyHf0cz2DqDihonJeIo+z+HEdRe9YuT8TY4A66i+Ab2/xZCEW7Ro60bPCBBfqqboHSamoV3+g==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.18.10",
@ -428,7 +413,6 @@
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
"integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/helper-string-parser": "^7.18.10",
"@babel/helper-validator-identifier": "^7.18.6",
@ -458,7 +442,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
"integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
"dev": true,
"peer": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.0",
"@jridgewell/sourcemap-codec": "^1.4.10"
@ -2350,7 +2333,6 @@
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
"integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==",
"dev": true,
"peer": true,
"dependencies": {
"safe-buffer": "~5.1.1"
}
@ -2359,8 +2341,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/cookie": {
"version": "0.4.2",
@ -3587,7 +3568,6 @@
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@ -3666,7 +3646,6 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=4"
}
@ -4302,7 +4281,6 @@
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true,
"peer": true,
"bin": {
"jsesc": "bin/jsesc"
},
@ -7297,7 +7275,6 @@
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"peer": true,
"engines": {
"node": ">=4"
}
@ -7587,9 +7564,9 @@
"dev": true
},
"node_modules/vuestic-ui": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/vuestic-ui/-/vuestic-ui-1.4.7.tgz",
"integrity": "sha512-t1gKmaoE3lqDDMUWnHZsJTal8cyHTeG9F/cbB1ggMH+loh6Po5tP7KTDA/93iG7dWtuwzuQhwt3/QE2rcbTkTA==",
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/vuestic-ui/-/vuestic-ui-1.4.13.tgz",
"integrity": "sha512-5f2d3isSaXq7RKWHZvGuMaiJraMugAy8DLnGBF9nLjiCZFT3b6E2WQiXpxvsyA4ZA7ZQsaEChNuaymAHdlv+xA==",
"dependencies": {
"cleave.js": "^1.6.0",
"colortranslator": "^1.9.2",
@ -8143,7 +8120,6 @@
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
"dev": true,
"peer": true,
"requires": {
"@jridgewell/gen-mapping": "^0.1.0",
"@jridgewell/trace-mapping": "^0.3.9"
@ -8169,7 +8145,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz",
"integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==",
"dev": true,
"peer": true,
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
@ -8192,8 +8167,7 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"dev": true,
"peer": true
"dev": true
}
}
},
@ -8202,7 +8176,6 @@
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.10.tgz",
"integrity": "sha512-0+sW7e3HjQbiHbj1NeU/vN8ornohYlacAfZIaXhdoGweQqgcNy69COVciYYqEXJ/v+9OBA7Frxm4CVAuNqKeNA==",
"dev": true,
"peer": true,
"requires": {
"@babel/types": "^7.18.10",
"@jridgewell/gen-mapping": "^0.3.2",
@ -8214,7 +8187,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"dev": true,
"peer": true,
"requires": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@ -8239,15 +8211,13 @@
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
"dev": true,
"peer": true
"dev": true
},
"@babel/helper-function-name": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz",
"integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==",
"dev": true,
"peer": true,
"requires": {
"@babel/template": "^7.18.6",
"@babel/types": "^7.18.9"
@ -8258,7 +8228,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
"dev": true,
"peer": true,
"requires": {
"@babel/types": "^7.18.6"
}
@ -8268,7 +8237,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
"dev": true,
"peer": true,
"requires": {
"@babel/types": "^7.18.6"
}
@ -8278,7 +8246,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz",
"integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==",
"dev": true,
"peer": true,
"requires": {
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-module-imports": "^7.18.6",
@ -8295,7 +8262,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz",
"integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==",
"dev": true,
"peer": true,
"requires": {
"@babel/types": "^7.18.6"
}
@ -8305,7 +8271,6 @@
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
"dev": true,
"peer": true,
"requires": {
"@babel/types": "^7.18.6"
}
@ -8314,8 +8279,7 @@
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz",
"integrity": "sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==",
"dev": true,
"peer": true
"dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.18.6",
@ -8334,7 +8298,6 @@
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz",
"integrity": "sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==",
"dev": true,
"peer": true,
"requires": {
"@babel/template": "^7.18.6",
"@babel/traverse": "^7.18.9",
@ -8414,7 +8377,6 @@
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
"integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
"dev": true,
"peer": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.18.10",
@ -8426,7 +8388,6 @@
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.10.tgz",
"integrity": "sha512-J7ycxg0/K9XCtLyHf0cz2DqDihonJeIo+z+HEdRe9YuT8TY4A66i+Ab2/xZCEW7Ro60bPCBBfqqboHSamoV3+g==",
"dev": true,
"peer": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.18.10",
@ -8445,7 +8406,6 @@
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
"integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
"dev": true,
"peer": true,
"requires": {
"@babel/helper-string-parser": "^7.18.10",
"@babel/helper-validator-identifier": "^7.18.6",
@ -8472,7 +8432,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
"integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
"dev": true,
"peer": true,
"requires": {
"@jridgewell/set-array": "^1.0.0",
"@jridgewell/sourcemap-codec": "^1.4.10"
@ -10007,7 +9966,6 @@
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
"integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==",
"dev": true,
"peer": true,
"requires": {
"safe-buffer": "~5.1.1"
},
@ -10016,8 +9974,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true,
"peer": true
"dev": true
}
}
},
@ -10946,8 +10903,7 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"peer": true
"dev": true
},
"geojson-vt": {
"version": "3.2.1",
@ -11007,8 +10963,7 @@
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true,
"peer": true
"dev": true
},
"globby": {
"version": "11.1.0",
@ -11477,8 +11432,7 @@
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true,
"peer": true
"dev": true
},
"json-parse-better-errors": {
"version": "1.0.2",
@ -13704,8 +13658,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"peer": true
"dev": true
},
"to-regex-range": {
"version": "5.0.1",
@ -13939,9 +13892,9 @@
"dev": true
},
"vuestic-ui": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/vuestic-ui/-/vuestic-ui-1.4.7.tgz",
"integrity": "sha512-t1gKmaoE3lqDDMUWnHZsJTal8cyHTeG9F/cbB1ggMH+loh6Po5tP7KTDA/93iG7dWtuwzuQhwt3/QE2rcbTkTA==",
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/vuestic-ui/-/vuestic-ui-1.4.13.tgz",
"integrity": "sha512-5f2d3isSaXq7RKWHZvGuMaiJraMugAy8DLnGBF9nLjiCZFT3b6E2WQiXpxvsyA4ZA7ZQsaEChNuaymAHdlv+xA==",
"requires": {
"cleave.js": "^1.6.0",
"colortranslator": "^1.9.2",

@ -10,11 +10,12 @@
"maplibre-gl": "^2.1.9",
"vue": "^3.2.13",
"vue-router": "^4.1.5",
"vuestic-ui": "^1.4.7"
"vuestic-ui": "^v1.8.6"
},
"devDependencies": {
"@vue/cli-service": "~5.0.0",
"nodemon": "^2.0.19",
"vue-cli-plugin-vuestic-ui": "~1.0.8"
"vue-cli-plugin-vuestic-ui": "~1.0.8",
"@babel/core": "^7.0.0"
}
}

@ -2,9 +2,7 @@
<div>
<h1 style="font-size: 3rem; margin: 1rem;">Текущий формат</h1>
<h2 style="margin-left: 1rem;">(скоординированный)</h2>
<va-radio class="radio-switcher" v-for="(option, index) in viewOptions" :key="index" v-model="selectedOption"
:option="option" :label="viewLabels[index]"></va-radio>
<div class="va-table-responsive" v-if="selectedOption === 'table-view'">
<div class="va-table-responsive" style="margin: 1rem;">
<table class="va-table va-table--striped detail-table">
<thead>
<tr>
@ -20,24 +18,6 @@
</tbody>
</table>
</div>
<va-list v-else-if="selectedOption === 'list-view'">
<va-list-item v-for="(_, header, index) in items[0]" :key="index" class="header-list">
<va-list-item-section icon>
<va-icon name="circle" color="gray" size="small"></va-icon>
</va-list-item-section>
<va-list-item-section>
<va-list-item-label class="header-item" :lines="2">{{ header }}</va-list-item-label>
</va-list-item-section>
</va-list-item>
</va-list>
<div v-else class="card-row">
<div class="card-item flex lg4" v-for="(_, header, index) in items[0]" :key="index">
<va-card :bordered="false">
<va-card-content>{{ header }}</va-card-content>
</va-card>
</div>
</div>
</div>
</template>
@ -48,18 +28,7 @@ export default defineComponent({
name: "active-schema-screen",
data() {
return {
columns: [],
viewOptions: [
"table-view",
"list-view",
"mosaic-view",
],
viewLabels: [
"Табличный вид",
"Списочный вид",
"Мозаичный вид",
],
selectedOption: "table-view"
columns: []
}
},
methods: {
@ -89,20 +58,9 @@ export default defineComponent({
<style lang="css" scoped>
.radio-switcher {
margin: 1rem;
}
.header-item {
padding: 0.5rem;
}
.card-row {
display: flex;
flex-wrap: wrap;
}
.card-item {
margin: 1rem;
}
</style>

@ -1,35 +1,41 @@
<template>
<div class="filter_panel">
<div class="sidebar-item">
<h3>Проект / тема</h3>
<project-filter @filter="$emit('filter', $event)" />
</div>
<div class="sidebar-item">
<h3>Категория</h3>
<category-filter @filter="$emit('filter', $event)" />
</div>
<div class="sidebar-item">
<h3>ГИС категория</h3>
<gis-category-filter @filter="$emit('filter', $event)" />
<h3>Описание</h3>
<description-filter @filter="$emit('filter', $event)" />
</div>
<div class="sidebar-item">
<h3>Томограф</h3>
<scanner-filter @filter="$emit('filter', $event)" />
<h3>Свита / пласт</h3>
<stratum-filter @filter="$emit('filter', $event)" />
</div>
<div class="sidebar-item">
<h3>Организация</h3>
<org-filter @filter="$emit('filter', $event)" />
<h3>Размер образца</h3>
<form-dimensions-filter @filter="$emit('filter', $event)" />
</div>
<div class="sidebar-item">
<h3>Бассейн</h3>
<basin-filter @filter="$emit('filter', $event)" />
<h3>Перечень данных</h3>
<datalist-filter @filter="$emit('filter', $event)" />
</div>
<div class="sidebar-item">
<h3>Свита / пласт</h3>
<stratum-filter @filter="$emit('filter', $event)" />
<h3>Разрешение съемки</h3>
<resolution-filter @filter="$emit('filter', $event)" />
</div>
<div class="sidebar-item">
<h3>Глубины</h3>
<depth-filter @filter="$emit('filter', $event)" />
@ -47,9 +53,27 @@ import OrgFilter from "@/components/filters/OrgFilter.vue";
import StratumFilter from "@/components/filters/StratumFilter.vue";
import DepthFilter from "@/components/filters/DepthFilter.vue";
import BasinFilter from "@/components/filters/BasinFilter.vue";
import ProjectFilter from "@/components/filters/ProjectFilter.vue";
import DescriptionFilter from "@/components/filters/DescriptionFilter.vue";
import DatalistFilter from "@/components/filters/DatalistFilter.vue";
import ResolutionFilter from "@/components/filters/ResolutionFilter.vue";
import FormDimensionsFilter from "@/components/filters/FormDimensionsFilter.vue";
export default {
name: "filter-panel",
components: { CategoryFilter, GisCategoryFilter, ScannerFilter, OrgFilter, StratumFilter, DepthFilter, BasinFilter },
components: {
CategoryFilter,
GisCategoryFilter,
ScannerFilter,
OrgFilter,
StratumFilter,
DepthFilter,
BasinFilter,
ProjectFilter,
DescriptionFilter,
DatalistFilter,
ResolutionFilter,
FormDimensionsFilter
},
methods: {},
data() {
return {}

@ -38,9 +38,17 @@
<va-card id="map" class="content-container map-container">
<item-map-component v-if="detailsLoaded" :internal_id="this.itemDetails.internal_id"
:x_coord="this.itemDetails.x_coord" :y_coord="this.itemDetails.y_coord" />
:x_coord="this.xComputed" :y_coord="this.yComputed" />
<h3 v-else>Карта загружается</h3>
</va-card>
<va-card id="similar" class="content-container" v-if="detailsLoaded">
<va-scroll-container vertical>
<va-card-title>Похожие образцы</va-card-title>
<va-data-table :items="filteredSimilarItems" :columns="filteredColumns" :hoverable="true"
:clickable="true" select-mode="single" @row:click="(e) => handleClick(e)"
class="similar-items-table" />
</va-scroll-container>
</va-card>
<va-card id="contact" class="content-container">
<va-card-title>Связь с представителем</va-card-title>
</va-card>
@ -55,21 +63,27 @@ export default {
data() {
return {
itemDetails: {},
hidden: [
"fadr",
shownDetails: [
"internal_id",
"x_coord",
"y_coord",
"owner",
"ownercontacts",
"id",
"depth_min",
"depth_max",
"category",
"basin",
"stratum",
"form_dimentions",
"resolution",
"scanner",
"datalist",
"comment"
],
imageExtension: "bmp",
hasImage: false,
detailsLoaded: false,
columnHeaders: []
columnHeaders: [],
showColumns: [
"description",
"deposit",
"category",
],
similarItems: [],
};
},
methods: {
@ -77,7 +91,6 @@ export default {
fetch(`/api/v1/item/${this.$route.params.id}`)
.then(res => res.json())
.then(data => this.itemDetails = data)
.then(() => this.detailsLoaded = true)
.catch((e) => console.log(e));
},
async fetchColumnHeaders() {
@ -105,11 +118,39 @@ export default {
}
return `/static/previews/${this.itemDetails.fadr}+/${this.itemDetails.internal_id}.${this.imageExtension}`
}
},
created() {
async fetchSimilarItems() {
fetch(`/api/v1/detailed_search/`,
{ method: 'POST', body: JSON.stringify({ fadr: this.itemDetails.fadr }), headers: { 'Content-Type': 'application/json' } })
.then(res => res.json())
.then(data => this.similarItems = data)
.then(() => this.detailsLoaded = true)
.catch((e) => console.log(e));
},
handleClick(e) {
if (e.event.ctrlKey) {
let routeData = this.$router.resolve({ name: 'item-details' });
window.open(routeData.href, '_blank');
} else {
this.detailsLoaded = false;
this.$router.replace(`/items/${e.item.id}`).then(a => {
this.fetchItemDetails();
});
}
},
},
mounted() {
this.fetchColumnHeaders();
this.fetchItemDetails().then(
this.fetchSimilarItems()
);
},
watch: {
'itemDetails': function (_oldval, _newval) {
this.fetchSimilarItems()
}
},
computed: {
slotPreviewPath() {
@ -125,9 +166,40 @@ export default {
},
formattedDetails() {
return Object.entries(this.itemDetails)
.filter(([k, _v]) => !(this.hidden.includes(k)))
.filter(([k, _v]) => (this.shownDetails.includes(k)))
.map(([k, v]) => [this.dictHeaders[k], v])
},
filteredColumns() {
return this.columnHeaders
.filter(header => this.showColumns.includes(header.database))
.sort((header1, header2) => {
return this.showColumns.indexOf(header1.database) - this.showColumns.indexOf(header2.database)
})
.map(header => {
return {
key: header.database,
label: header.spreadsheet,
sortable: true,
}
})
},
forceSimilarItemsReload() {
// https://stackoverflow.com/questions/48641295/async-computed-in-components-vuejs
this.fetchSimilarItems();
for (let _i in this.itemDetails) {
return this.itemDetails.fadr;
}
return false;
},
filteredSimilarItems() {
return [...this.similarItems].filter(item => item.internal_id !== this.itemDetails.internal_id)
},
xComputed() {
return this.itemDetails.x_coord ? this.itemDetails.x_coord : 55.75
},
yComputed() {
return this.itemDetails.y_coord ? this.itemDetails.y_coord : 37.62
},
},
components: { ItemMapComponent }
}
@ -146,6 +218,11 @@ export default {
margin: 1rem auto;
}
.similar-items-table {
width: 45rem;
overflow-x: hidden;
}
.item-description {
margin-top: 2rem;
margin-bottom: 2rem;

@ -7,7 +7,7 @@
<script>
import maplibregl from "maplibre-gl";
import { markRaw, onMounted, onUnmounted, reactive, shallowRef, } from "vue";
import { markRaw, onMounted, onUnmounted, reactive, shallowRef, watchEffect, } from "vue";
export default {
name: "item-map-component",
props: {
@ -25,6 +25,31 @@ export default {
setup(props, _context) {
const mapContainer = shallowRef(null);
const map = shallowRef(null);
let isSampleLayerAdded = false;
const addSamplesLayer = () => {
map.value.addSource('samples', {
'type': 'vector',
"tiles": ["http://localhost/martin/public.geodata/{z}/{x}/{y}.pbf"],
'promoteId': 'fadr',
});
map.value.addLayer({
'id': 'samples-layer',
'source': 'samples',
'source-layer': 'public.geodata',
'type': 'circle',
'paint': {
'circle-stroke-width': 1,
'circle-stroke-color': '#FFFFFF',
'circle-color': '#d95f0e',
'circle-opacity': 0.8,
'circle-radius': 16
},
filter: ["==", ["get", "internal_id"], props.internal_id]
});
}
onMounted(() => {
const apiKey = "pk.eyJ1IjoiZ2hlcm1hbnQiLCJhIjoiY2pncDUwcnRmNDQ4ZjJ4czdjZXMzaHZpNyJ9.3rFyYRRtvLUngHm027HZ7A";
@ -58,25 +83,11 @@ export default {
}
);
map.value.addSource('samples', {
'type': 'vector',
"tiles": ["http://localhost:8080/martin/public.geodata/{z}/{x}/{y}.pbf"],
'promoteId': 'fadr',
});
map.value.addLayer({
'id': 'samples-layer',
'source': 'samples',
'source-layer': 'public.geodata',
'type': 'circle',
'paint': {
'circle-stroke-width': 1,
'circle-stroke-color': '#FFFFFF',
'circle-color': '#d95f0e',
'circle-opacity': 0.8,
'circle-radius': 16
},
filter: ["==", ["get", "internal_id"], props.internal_id]
watchEffect(() => {
if (!isSampleLayerAdded && typeof props.internal_id !== typeof undefined) {
isSampleLayerAdded = true;
addSamplesLayer();
}
});
});

@ -41,6 +41,7 @@ export default {
.then(data => {
if (data.access_token) {
localStorage.setItem('user', JSON.stringify(data));
document.cookie = `user_session=${JSON.stringify(data.access_token)}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=strict`
this.$router.push('/admin')
}
})

@ -1,5 +1,10 @@
<template>
<div class="map-wrap">
<div id="layer-control" class="d-flex flex-direction-column">
<va-radio class="radio-switcher" v-for="(option, index) in viewOptions" :key="index"
v-model="selectedOption" :option="option" :label="viewLabels[index]"></va-radio>
<va-checkbox class="mb-4 fields-switch" v-model="valuecb" label="Месторождения" />
</div>
<div id="map" class="map" ref="mapContainer"></div>
</div>
</template>
@ -7,7 +12,7 @@
<script>
import maplibregl from "maplibre-gl";
import { markRaw, onMounted, onUnmounted, reactive, shallowRef, watch } from "vue";
import { markRaw, onMounted, onUnmounted, reactive, ref, shallowRef, watch } from "vue";
export default {
name: "map-component",
props: {
@ -20,14 +25,32 @@ export default {
const mapContainer = shallowRef(null);
const map = shallowRef(null);
let currentFadr = reactive({ "fadr": [] });
const valuecb = ref(true);
const selectedOption = ref("topo");
const viewOptions = ["topo", "satellite", "mono"];
const viewLabels = ["Карта", "Спутник", "Монохром"];
const apiKey = "pk.eyJ1IjoiZ2hlcm1hbnQiLCJhIjoiY2pncDUwcnRmNDQ4ZjJ4czdjZXMzaHZpNyJ9.3rFyYRRtvLUngHm027HZ7A";
const basemapTiles = {
"topo": [`https://api.mapbox.com/styles/v1/ghermant/cl9vd1nji002n15s2xxj25f4m/tiles/256/{z}/{x}/{y}@2x?access_token=${apiKey}`],
"satellite": [`https://api.mapbox.com/styles/v1/ghermant/cl9olarp0002i14vq9a2d0e7g/tiles/256/{z}/{x}/{y}@2x?access_token=${apiKey}`],
"mono": [`https://api.mapbox.com/styles/v1/ghermant/cl9vd7xwi004u14nxu3j83scj/tiles/256/{z}/{x}/{y}@2x?access_token=${apiKey}`]
};
watch(() => [...props.idlist], (_currentValue, _oldValue) => {
updateSamplesLayer()
});
watch(() => valuecb.value, (_currentValue, _oldValue) => {
updateFieldsLayer()
});
watch(() => selectedOption.value, (_currentValue, _oldValue) => {
updateBaseLayer()
})
const buildMap = () => {
map.value = markRaw(new maplibregl.Map({
container: mapContainer.value, // container id
@ -43,24 +66,43 @@ export default {
}));
map.value.on('load', () => {
map.value.addSource('basemap-source', {
map.value.addSource('basemap', {
'type': 'raster',
'tiles': [`https://api.mapbox.com/styles/v1/ghermant/cl8vg2r97001m14o4c2qzw9t6/tiles/256/{z}/{x}/{y}@2x?access_token=${apiKey}`]
'tiles': basemapTiles[selectedOption.value]
})
map.value.addLayer(
{
'id': 'basemap-layer',
'type': 'raster',
'source': 'basemap-source',
'source': 'basemap',
'paint': {}
}
);
map.value.addSource("fields", {
type: "vector",
tiles: [`https://api.mapbox.com/v4/ghermant.cpc0xaaw/{z}/{x}/{y}.mvt?access_token=${apiKey}`]
})
map.value.addLayer({
'id': 'fields-layer',
'type': 'fill',
'source': 'fields',
'source-layer': 'mygeomapZoom2-2utkfs',
'paint': {
'fill-color': '#fc9272',
'fill-opacity': 0.5
},
'layout': {
'visibility': 'visible'
}
})
map.value.addSource('samples', {
'type': 'vector',
"tiles": ["http://localhost:8080/martin/public.geodata/{z}/{x}/{y}.pbf"],
"tiles": ["http://localhost/martin/public.geodata/{z}/{x}/{y}.pbf"],
'promoteId': 'fadr',
});
@ -88,7 +130,23 @@ export default {
const popup = new maplibregl.Popup({
closeButton: false,
closeOnClick: false,
})
});
map.value.on("click", "fields-layer", (e) => {
new maplibregl.Popup({})
.setLngLat(e.lngLat)
.setHTML(e.features[0].properties["descriptio"])
.addTo(map.value);
});
map.value.on("mouseover", "fields-layer", (e) => {
map.value.getCanvas().style.cursor = "pointer";
});
map.value.on("mouseleave", "fields-layer", (e) => {
map.value.getCanvas().style.cursor = "";
});
map.value.on('mouseover', 'samples-layer', (e) => {
// Change the cursor style as a UI indicator.
@ -96,7 +154,7 @@ export default {
// Populate the popup and set its coordinates
// based on the feature found.
popup
.setLngLat(e.lngLat)
.setLngLat(e.features[0].geometry.coordinates)
.setHTML(`Записей в точке: <b>${e.features.length}</b>`)
.addTo(map.value);
});
@ -164,33 +222,31 @@ export default {
};
const updateSamplesLayer = () => {
if (map.value.getLayer('samples-layer')) {
map.value.removeLayer('samples-layer');
if (props.idlist.length < 2000) {
map.value.setFilter('samples-layer', ["in", "internal_id", ...props.idlist])
} else {
map.value.setFilter('samples-layer', null)
}
};
if (props.idlist.length) {
map.value.addLayer({
'id': 'samples-layer',
'source': 'samples',
'source-layer': 'public.geodata',
'type': 'circle',
'paint': {
'circle-stroke-width': 1,
'circle-stroke-color': '#FFFFFF',
'circle-color': [
'case',
['boolean', ['feature-state', 'beenClicked'], false],
'#fec44f',
'#d95f0e'
],
'circle-opacity': 0.8,
'circle-radius': 8
},
filter: ["match", ["get", "internal_id"], props.idlist, true, false]
});
}
const updateFieldsLayer = () => {
const visibility = valuecb.value ? 'visible' : 'none';
map.value.setLayoutProperty('fields-layer', 'visibility', visibility);
};
const updateBaseLayer = () => {
const selectedBasemap = selectedOption.value;
// https://stackoverflow.com/questions/38631344/recommended-way-to-switch-tile-urls-in-mapbox-gl-js
// Set the tile url to a cache-busting url (to circumvent browser caching behaviour):
map.value.getSource('basemap').tiles = basemapTiles[selectedBasemap]
// Remove the tiles for a particular source
map.value.style.sourceCaches['basemap'].clearTiles()
// Load the new tiles for the current viewport (map.value.transform -> viewport)
map.value.style.sourceCaches['basemap'].update(map.value.transform)
// Force a repaint, so that the map will be repainted without you having to touch the map
map.value.triggerRepaint()
}
onMounted(() => {
buildMap()
});
@ -202,7 +258,7 @@ export default {
return {
map, mapContainer, currentFadr
map, mapContainer, currentFadr, valuecb, selectedOption, viewOptions, viewLabels
}
}
}
@ -225,4 +281,24 @@ export default {
height: 100%;
/* z-index: -1; */
}
#layer-control {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 5;
flex-direction: column;
}
.radio-switcher {
padding-left: 0.3rem;
}
.fields-switch {
margin-top: 1rem;
}
.va-radio__text, .va-checkbox__label {
color: white;
}
</style>

@ -3,16 +3,56 @@
<va-navbar shape color="#79589f" class="navbar">
<template #left>
<va-navbar-item class="top-icon">
<va-popover message="Главная страница">
<va-icon size="3rem" color="gray" name="public" @click="$router.push('/')" />
<va-popover message="Карта" prevent-overflow stick-to-edges>
<va-icon size="3rem" name="public" @click="$router.push('/')" />
</va-popover>
</va-navbar-item>
<slot></slot>
</template>
<template #right>
<va-dropdown :close-on-content-click="false" placement="bottom-end" prevent-overflow>
<template #anchor>
<va-navbar-item class="top-icon">
<va-popover message="Аккаунт">
<va-icon size="3rem" color="gray" name="account_box" @click="$emit('accountClick')" />
<va-popover message="Корзина" prevent-overflow stick-to-edges>
<va-icon size="3rem" name="shopping_basket" @click="" />
</va-popover>
</va-navbar-item>
</template>
<va-dropdown-content>
<va-card class="flex">
<va-card-title>Корзина</va-card-title>
<va-card-content v-if="!itemsInCart.length" class="cart-inner cart-empty">
<p class="cart-empty__text">Корзина пуста</p>
<p class="cart-empty__text">Попробуйте перейти на страницу образца</p>
<p v-if="isItemPath" class="cart-empty__text">
и <va-button @click="addToCart">Добавить в корзину</va-button>
</p>
</va-card-content>
<template v-else>
<va-card-actions align="between">
<template v-if="isItemPath">
<va-button v-if="!isPathInCart" @click="addToCart">Добавить в корзину
</va-button>
<va-button v-else @click="removeFromCart">Убрать из корзины</va-button>
</template>
<va-button @click="copyCart">{{ copyCartText }}</va-button>
<va-button color="warning" @click="emptyCart">Очистить корзину</va-button>
</va-card-actions>
<va-card-content class="cart-inner">
<va-data-table :items="itemsInCart" :columns="filteredColumns" :hoverable="true"
:clickable="true" select-mode="single" @row:click="(e) => handleClick(e)"
class="similar-items-table" />
</va-card-content>
</template>
</va-card>
</va-dropdown-content>
</va-dropdown>
<va-navbar-item class="top-icon">
<va-popover message="Аккаунт" prevent-overflow stick-to-edges>
<va-icon size="3rem" name="account_box" @click="$emit('accountClick')" />
</va-popover>
</va-navbar-item>
</template>
@ -24,6 +64,98 @@
<script>
export default {
name: "navbar",
data() {
return {
columnHeaders: [],
showColumns: [
"internal_id",
"description",
],
itemsInCart: [],
cartListCopied: false,
}
},
methods: {
addToCart() {
if (!this.isPathInCart) {
fetch(`/api/v1/item/${this.$route.params.id}`)
.then(res => res.json())
.then(data => {
this.itemsInCart.push(data);
localStorage.setItem("cart", JSON.stringify(this.itemsInCart));
this.cartListCopied = false;
})
.catch((e) => console.log(e));
}
},
removeFromCart() {
const routeId = Number.parseInt(this.$route.params.id);
this.itemsInCart = this.itemsInCart.filter(item => item.id !== routeId)
localStorage.setItem("cart", JSON.stringify(this.itemsInCart));
this.cartListCopied = false;
},
copyCart() {
const textToCopy = this.itemsInCart
.map(item => item.internal_id)
.reduce((list, item_id) => list + "," + item_id)
navigator.clipboard.writeText(textToCopy)
.then(() => this.cartListCopied = true)
.catch(e => console.log("Error copying item list ("
+ textToCopy
+ ") from cart: "
+ e))
},
emptyCart() {
this.itemsInCart = [];
localStorage.removeItem("cart");
},
restoreCart() {
if (localStorage.getItem("cart")) {
this.itemsInCart = JSON.parse(localStorage.getItem("cart"))
}
},
async fetchColumnHeaders() {
fetch(`/api/v1/headers/`)
.then(res => res.json())
.then(data => this.columnHeaders = data)
.catch((e) => console.log(e));
},
handleClick(e) {
if (String(e.item.id) !== this.$route.params.id) {
window.open(`/items/${e.item.id}`, '_blank')
}
}
},
mounted() {
this.fetchColumnHeaders();
this.restoreCart()
},
computed: {
isItemPath() {
return this.$route.name === "item-details";
},
filteredColumns() {
return this.columnHeaders
.filter(header => this.showColumns.includes(header.database))
.sort((header1, header2) => {
return this.showColumns.indexOf(header1.database) - this.showColumns.indexOf(header2.database)
})
.map(header => {
return {
key: header.database,
label: header.spreadsheet,
sortable: true,
}
})
},
isPathInCart() {
const routeId = Number.parseInt(this.$route.params.id);
return this.itemsInCart.some(item => item.id === routeId)
},
copyCartText() {
return (this.cartListCopied ? 'Скопировано ✓✓' : 'Скопировать список образцов')
}
},
}
</script>
@ -32,4 +164,19 @@ export default {
.navbar {
position: relative;
}
.cart-inner {
display: flex;
flex-direction: column;
min-width: 30rem;
min-height: 30rem;
}
.cart-empty {
justify-content: center;
}
.cart-empty__text {
margin: 1rem auto;
}
</style>

@ -5,11 +5,12 @@
@mapClick="$emit('mapClick', $event)" />
</va-card>
<va-card class="content-container">
<va-data-table v-if="items.length" :items="items" :columns="columns" :hoverable="true" :clickable="true"
:per-page="perPage" :current-page="currentPage" @row:click="(e) => $router.push(`/items/${e.item.id}`)"
<va-scroll-container vertical>
<va-data-table virtual-scroller sticky-header v-if="items.length" :items="items" :columns="columns" :hoverable="true" :clickable="true"
@row:click="(e) => $router.push(`/items/${e.item.id}`)"
no-data-filtered-html="Не найдено образцов, соответствующих данному запросу" />
<va-pagination v-if="items.length" v-model="currentPage" :pages="pages" input style="margin: 0.5rem auto" />
<h1 v-else class="no-items">Не найдено образцов, соответствующих данному запросу</h1>
</va-scroll-container>
</va-card>
</div>
</template>
@ -32,23 +33,12 @@ export default {
},
data() {
return {
perPage: 10,
currentPage: 1,
settleFinished: false,
}
},
methods: {},
computed: {
pages() {
this.settleFinished = true;
this.currentPage = 1;
return (this.perPage && this.perPage !== 0)
? Math.ceil(this.items.length / this.perPage)
: this.items.length
},
itemIdList() {
return this.items.map(item => item.internal_id)
},

@ -1,6 +1,6 @@
<template>
<div>
<va-sidebar color="white" text-color="black" class="sidebar-proper" width="18rem">
<va-sidebar class="sidebar-proper" width="18rem">
<slot></slot>
</va-sidebar>
</div>
@ -17,7 +17,7 @@ export default {
<style lang="css">
.sidebar-proper {
height: max(100vh, 100%);
box-shadow: 0 0 15px rgba(0, 0, 0, 0.75);
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
clip-path: inset(0px -15px 0px 0px);
overflow-y: hidden;
}

@ -0,0 +1,40 @@
<template>
<div>
<va-select class="multi-selector" :options="datalistOptions" v-model="selectedOptions"
@update:model-value="applyFilter" multiple searchable>
</va-select>
</div>
</template>
<script>
export default {
name: "datalist-filter",
data() {
return {
datalistOptions: [
// SELECT DISTINCT BTRIM(UNNEST(STRING_TO_ARRAY(datalist,','))) FROM geodata;
"Project",
"Rec",
"Data",
"Raw",
"Report",
],
selectedOptions: [],
}
},
methods: {
applyFilter() {
const filter = { "datalist": this.selectedOptions }
this.$emit('filter', filter, this.name)
},
},
}
</script>
<style lang="css">
.multi-selector {
--va-select-min-width: 12 rem;
}
</style>

@ -0,0 +1,43 @@
<template>
<div>
<va-select class="multi-selector" :options="descriptionOptions" v-model="selectedOptions"
@update:model-value="applyFilter" multiple searchable>
</va-select>
</div>
</template>
<script>
export default {
name: "description-filter",
data() {
return {
descriptionOptions: [
"терригенная порода-коллектор",
"черви",
"бусины из скорлупы страуса",
"осколки скорлупы яиц страуса",
"лопатка лощади ",
"грунты",
"глинисто-карбонатная порода",
"Искусстенная модель осадочных пород",
"угли",
],
selectedOptions: [],
}
},
methods: {
applyFilter() {
const filter = { "description": this.selectedOptions }
this.$emit('filter', filter, this.name)
},
},
}
</script>
<style lang="css">
.multi-selector {
--va-select-min-width: 12 rem;
}
</style>

@ -0,0 +1,42 @@
<template>
<div>
<va-select class="multi-selector" :options="formDimentionsOptions" v-model="selectedOptions"
@update:model-value="applyFilter" multiple searchable>
</va-select>
</div>
</template>
<script>
export default {
name: "form-dimentions-filter",
data() {
return {
formDimentionsOptions: [
"фрагмент 20 см",
"фрагменты 5-7 см",
"полноразмерный керн, 65 мм",
"цилиндр, 8 мм",
"цилиндр, 10 мм",
"цилиндр, 30 мм",
"полноразмерный керн",
"полноразмерный керн, 100 мм",
],
selectedOptions: [],
}
},
methods: {
applyFilter() {
const filter = { "form_dimentions": this.selectedOptions }
this.$emit('filter', filter, this.name)
},
},
}
</script>
<style lang="css">
.multi-selector {
--va-select-min-width: 12 rem;
}
</style>

@ -0,0 +1,42 @@
<template>
<div>
<va-select class="multi-selector" :options="projectOptions" v-model="selectedOptions"
@update:model-value="applyFilter" multiple searchable>
</va-select>
</div>
</template>
<script>
export default {
name: "project-filter",
data() {
return {
projectOptions: [
"AAK",
"AAM",
"ABB",
"ABE",
"AER",
"AJL",
"ASX",
"ATM",
],
selectedOptions: [],
}
},
methods: {
applyFilter() {
const filter = { "project": this.selectedOptions }
this.$emit('filter', filter, this.name)
},
},
}
</script>
<style lang="css">
.multi-selector {
--va-select-min-width: 12 rem;
}
</style>

@ -0,0 +1,43 @@
<template>
<div class="range-input">
<va-input placeholder="1.59" v-model="reso_min" input-class="text--center"
:mask="{ numeral: true, delimiter: '', numeralDecimalMark: ',' }" @change="applyFilter" />
<va-input placeholder="264" v-model="reso_max" input-class="text--center"
:mask="{ numeral: true, delimiter: '', numeralDecimalMark: ',' }" @change="applyFilter" />
</div>
</template>
<script>
export default {
name: "resolution-filter",
data() {
return {
// SELECT MIN(resolution), MAX(resolution) FROM geodata;
reso_min: "1.59",
reso_max: "264",
}
},
methods: {
applyFilter() {
const filter = { "resolution": [this.reso_min, this.reso_max] }
this.$emit('filter', filter, this.name)
},
},
}
</script>
<style lang="css">
.range-input {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 5rem
}
.range-input>div {
--va-input-wrapper-min-width: 4rem;
width: 4rem;
}
</style>

@ -22,6 +22,7 @@ import {
VaCard,
VaCardTitle,
VaCardContent,
VaCardActions,
VaImage,
VaSelect,
VaPagination,
@ -30,6 +31,10 @@ import {
VaChip,
VaModal,
VaPopover,
VaScrollContainer,
VaDropdown,
VaDropdownContent,
VaCheckbox,
} from 'vuestic-ui'
import 'vuestic-ui/dist/styles/essential.css'
import 'vuestic-ui/dist/styles/grid.css'
@ -66,6 +71,7 @@ app.use(createVuesticEssential({
VaCard,
VaCardTitle,
VaCardContent,
VaCardActions,
VaImage,
VaSelect,
VaPagination,
@ -74,6 +80,10 @@ app.use(createVuesticEssential({
VaChip,
VaModal,
VaPopover,
VaScrollContainer,
VaDropdown,
VaDropdownContent,
VaCheckbox,
}
}));

@ -1,5 +1,5 @@
<template>
<navbar @accountClick="$router.push('/admin/')">
<navbar class="navbar" @accountClick="$router.push('/admin/')">
<va-input placeholder="Начните вводить запрос" v-model="searchQuery"></va-input>
</navbar>
<div class="central-view">
@ -39,14 +39,15 @@ export default {
"description",
"form_dimentions",
"datalist",
"resolution",
],
showColumns: [
"description",
"deposit",
"org",
"internal_id",
"category",
"scanner",
"description",
"stratum",
"form_dimentions",
"resolution",
"datalist"
],
showModal: false,
}
@ -89,6 +90,28 @@ export default {
if (item.depth_max < filterValues[0] || item.depth_min > filterValues[1]) {
return false;
}
} else if (filterParam === "resolution") {
if (
// we control resolution input format, so we can be reasonbly sure
// it has upto one comma to denote decimal point and no dots
// therefore it's safe to normalize it into
// JavaScript definition of a decimal literal
item.resolution < parseFloat(filterValues[0].replace(/,/, '.'))
|| item.resolution > parseFloat(filterValues[1].replace(/,/, '.'))
) {
return false;
}
} else if (filterParam === "datalist") {
// for all selected datalist options check if we can find it in the datalist string
for (let datalistOption of filterValues) {
if (item.datalist.search(datalistOption) === -1) {
return false;
}
}
} else if (filterParam === "project") {
if (!filterValues.includes(item["fadr"])) {
return false;
}
} else if (!filterValues.includes(item[filterParam])) {
return false;
}
@ -152,4 +175,8 @@ export default {
.main-view {
width: 100%;
}
.navbar {
z-index: 100;
}
</style>

@ -47,6 +47,12 @@ export default {
route: "item-details",
hash: "#map",
},
{
title: "Похожие образцы",
icon: "view_list",
route: "item-details",
hash: "#similar",
},
{
title: "Связь с представителем",
icon: "contact_page",

Loading…
Cancel
Save